diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index a92191a439..e34626a59e 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -4,3 +4,5 @@ M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(
M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.
T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
+T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
+T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.
diff --git a/Directory.Build.props b/Directory.Build.props
index 21b8b402e0..2cd40c8675 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -16,9 +16,9 @@
-
+
-
+
$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset
diff --git a/osu.Android.props b/osu.Android.props
index 25942863c5..a406cdf08a 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index 84f215f930..19ed7ffcf5 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -18,7 +18,8 @@ namespace osu.Android
try
{
- string versionName = packageInfo.VersionCode.ToString();
+ // todo: needs checking before play store redeploy.
+ string versionName = packageInfo.VersionName;
// undo play store version garbling
return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
}
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index f05ee48914..9351e17419 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -6,15 +6,14 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
+using Microsoft.Win32;
using osu.Desktop.Overlays;
using osu.Framework.Platform;
using osu.Game;
using osuTK.Input;
-using Microsoft.Win32;
using osu.Desktop.Updater;
using osu.Framework;
using osu.Framework.Logging;
-using osu.Framework.Platform.Windows;
using osu.Framework.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Updater;
@@ -37,7 +36,11 @@ namespace osu.Desktop
try
{
if (Host is DesktopGameHost desktopHost)
- return new StableStorage(desktopHost);
+ {
+ string stablePath = getStableInstallPath();
+ if (!string.IsNullOrEmpty(stablePath))
+ return new DesktopStorage(stablePath, desktopHost);
+ }
}
catch (Exception)
{
@@ -47,6 +50,35 @@ namespace osu.Desktop
return null;
}
+ private string getStableInstallPath()
+ {
+ static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
+
+ string stableInstallPath;
+
+ try
+ {
+ using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
+ stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
+
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+ }
+ catch
+ {
+ }
+
+ stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ return null;
+ }
+
protected override UpdateManager CreateUpdateManager()
{
switch (RuntimeInfo.OS)
@@ -111,45 +143,5 @@ namespace osu.Desktop
Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning);
}
-
- ///
- /// A method of accessing an osu-stable install in a controlled fashion.
- ///
- private class StableStorage : WindowsStorage
- {
- protected override string LocateBasePath()
- {
- static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
-
- string stableInstallPath;
-
- try
- {
- using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
- stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
-
- if (checkExists(stableInstallPath))
- return stableInstallPath;
- }
- catch
- {
- }
-
- stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
- if (checkExists(stableInstallPath))
- return stableInstallPath;
-
- stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
- if (checkExists(stableInstallPath))
- return stableInstallPath;
-
- return null;
- }
-
- public StableStorage(DesktopGameHost host)
- : base(string.Empty, host)
- {
- }
- }
}
}
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 60b47a8b3a..ade8460dd7 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -43,7 +43,7 @@ namespace osu.Desktop.Updater
private async void checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
{
- //should we schedule a retry on completion of this check?
+ // should we schedule a retry on completion of this check?
bool scheduleRecheck = true;
try
@@ -52,7 +52,7 @@ namespace osu.Desktop.Updater
var info = await updateManager.CheckForUpdate(!useDeltaPatching);
if (info.ReleasesToApply.Count == 0)
- //no updates available. bail and retry later.
+ // no updates available. bail and retry later.
return;
if (notification == null)
@@ -81,8 +81,8 @@ namespace osu.Desktop.Updater
{
logger.Add(@"delta patching failed; will attempt full download!");
- //could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
- //try again without deltas.
+ // could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
+ // try again without deltas.
checkForUpdateAsync(false, notification);
scheduleRecheck = false;
}
@@ -101,7 +101,7 @@ namespace osu.Desktop.Updater
{
if (scheduleRecheck)
{
- //check again in 30 minutes.
+ // check again in 30 minutes.
Scheduler.AddDelayed(() => checkForUpdateAsync(), 60000 * 30);
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
index 51fe0b035d..ee416e5a38 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
- [TestCase(4.2058561036909863d, "diffcalc-test")]
+ [TestCase(4.050601681491468d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 8c371db257..cbd3dc5518 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index d99325ff87..a317ef252d 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyCalculator : DifficultyCalculator
{
- private const double star_scaling_factor = 0.145;
+ private const double star_scaling_factor = 0.153;
protected override int SectionLength => 750;
@@ -73,6 +73,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
+ // For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
+ halfCatcherWidth *= 1 - (Math.Max(0, beatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5.5f) * 0.0625f);
+
return new Skill[]
{
new Movement(halfCatcherWidth),
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index a6283eb7c4..e7ce680365 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -52,8 +52,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
// Longer maps are worth more
double lengthBonus =
- 0.95 + 0.4 * Math.Min(1.0, numTotalHits / 3000.0) +
- (numTotalHits > 3000 ? Math.Log10(numTotalHits / 3000.0) * 0.5 : 0.0);
+ 0.95f + 0.3f * Math.Min(1.0f, numTotalHits / 2500.0f) +
+ (numTotalHits > 2500 ? (float)Math.Log10(numTotalHits / 2500.0f) * 0.475f : 0.0f);
// Longer maps are worth more
value *= lengthBonus;
@@ -63,19 +63,28 @@ namespace osu.Game.Rulesets.Catch.Difficulty
// Combo scaling
if (Attributes.MaxCombo > 0)
- value *= Math.Min(Math.Pow(Attributes.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
+ value *= Math.Min(Math.Pow(Score.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
- double approachRateFactor = 1.0;
- if (Attributes.ApproachRate > 9.0)
- approachRateFactor += 0.1 * (Attributes.ApproachRate - 9.0); // 10% for each AR above 9
- else if (Attributes.ApproachRate < 8.0)
- approachRateFactor += 0.025 * (8.0 - Attributes.ApproachRate); // 2.5% for each AR below 8
+ float approachRate = (float)Attributes.ApproachRate;
+ float approachRateFactor = 1.0f;
+ if (approachRate > 9.0f)
+ approachRateFactor += 0.1f * (approachRate - 9.0f); // 10% for each AR above 9
+ if (approachRate > 10.0f)
+ approachRateFactor += 0.1f * (approachRate - 10.0f); // Additional 10% at AR 11, 30% total
+ else if (approachRate < 8.0f)
+ approachRateFactor += 0.025f * (8.0f - approachRate); // 2.5% for each AR below 8
value *= approachRateFactor;
if (mods.Any(m => m is ModHidden))
- // Hiddens gives nothing on max approach rate, and more the lower it is
+ {
value *= 1.05 + 0.075 * (10.0 - Math.Min(10.0, Attributes.ApproachRate)); // 7.5% for each AR below 10
+ // Hiddens gives almost nothing on max approach rate, and more the lower it is
+ if (approachRate <= 10.0f)
+ value *= 1.05f + 0.075f * (10.0f - approachRate); // 7.5% for each AR below 10
+ else if (approachRate > 10.0f)
+ value *= 1.01f + 0.04f * (11.0f - Math.Min(11.0f, approachRate)); // 5% at AR 10, 1% at AR 11
+ }
if (mods.Any(m => m is ModFlashlight))
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
index 24e526ed19..360af1a8c9 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
@@ -21,10 +21,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
public readonly float LastNormalizedPosition;
///
- /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms.
+ /// Milliseconds elapsed since the start time of the previous , with a minimum of 40ms.
///
public readonly double StrainTime;
+ public readonly double ClockRate;
+
public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth)
: base(hitObject, lastObject, clockRate)
{
@@ -34,8 +36,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
NormalizedPosition = BaseObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
LastNormalizedPosition = LastObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
- // Every strain interval is hard capped at the equivalent of 600 BPM streaming speed as a safety measure
- StrainTime = Math.Max(25, DeltaTime);
+ // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
+ StrainTime = Math.Max(40, DeltaTime);
+ ClockRate = clockRate;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index fd164907e0..5cd2f1f581 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -13,9 +13,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
private const float absolute_player_positioning_error = 16f;
private const float normalized_hitobject_radius = 41.0f;
- private const double direction_change_bonus = 12.5;
+ private const double direction_change_bonus = 21.0;
- protected override double SkillMultiplier => 850;
+ protected override double SkillMultiplier => 900;
protected override double StrainDecayBase => 0.2;
protected override double DecayWeight => 0.94;
@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
private float? lastPlayerPosition;
private float lastDistanceMoved;
+ private double lastStrainTime;
public Movement(float halfCatcherWidth)
{
@@ -45,47 +46,47 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
float distanceMoved = playerPosition - lastPlayerPosition.Value;
- double distanceAddition = Math.Pow(Math.Abs(distanceMoved), 1.3) / 500;
- double sqrtStrain = Math.Sqrt(catchCurrent.StrainTime);
+ double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catchCurrent.ClockRate);
- double bonus = 0;
+ double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
+ double sqrtStrain = Math.Sqrt(weightedStrainTime);
- // Direction changes give an extra point!
+ double edgeDashBonus = 0;
+
+ // Direction change bonus.
if (Math.Abs(distanceMoved) > 0.1)
{
if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved))
{
- double bonusFactor = Math.Min(absolute_player_positioning_error, Math.Abs(distanceMoved)) / absolute_player_positioning_error;
+ double bonusFactor = Math.Min(50, Math.Abs(distanceMoved)) / 50;
+ double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(lastDistanceMoved)) / 70, 0.38);
- distanceAddition += direction_change_bonus / sqrtStrain * bonusFactor;
-
- // Bonus for tougher direction switches and "almost" hyperdashes at this point
- if (catchCurrent.LastObject.DistanceToHyperDash <= 10 / CatchPlayfield.BASE_WIDTH)
- bonus = 0.3 * bonusFactor;
+ distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
}
// Base bonus for every movement, giving some weight to streams.
- distanceAddition += 7.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
+ distanceAddition += 12.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
}
- // Bonus for "almost" hyperdashes at corner points
- if (catchCurrent.LastObject.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH)
+ // Bonus for edge dashes.
+ if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f / CatchPlayfield.BASE_WIDTH)
{
if (!catchCurrent.LastObject.HyperDash)
- bonus += 1.0;
+ edgeDashBonus += 5.7;
else
{
// After a hyperdash we ARE in the correct position. Always!
playerPosition = catchCurrent.NormalizedPosition;
}
- distanceAddition *= 1.0 + bonus * ((10 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 10);
+ distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
}
lastPlayerPosition = playerPosition;
lastDistanceMoved = distanceMoved;
+ lastStrainTime = catchCurrent.StrainTime;
- return distanceAddition / catchCurrent.StrainTime;
+ return distanceAddition / weightedStrainTime;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index 16414261a5..c1d24395e4 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Mods
RelativeSizeAxes = Axes.Both;
}
- //disable keyboard controls
+ // disable keyboard controls
public bool OnPressed(CatchAction action) => true;
public void OnReleased(CatchAction action)
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index b90b5812a6..7a33cb0577 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Catch.Replays
if (lastPosition - catcher_width_half < h.X && lastPosition + catcher_width_half > h.X)
{
- //we are already in the correct range.
+ // we are already in the correct range.
lastTime = h.StartTime;
addFrame(h.StartTime, lastPosition);
return;
@@ -72,14 +72,14 @@ namespace osu.Game.Rulesets.Catch.Replays
}
else if (dashRequired)
{
- //we do a movement in two parts - the dash part then the normal part...
+ // we do a movement in two parts - the dash part then the normal part...
double timeAtNormalSpeed = positionChange / movement_speed;
double timeWeNeedToSave = timeAtNormalSpeed - timeAvailable;
double timeAtDashSpeed = timeWeNeedToSave / 2;
float midPosition = (float)Interpolation.Lerp(lastPosition, h.X, (float)timeAtDashSpeed / timeAvailable);
- //dash movement
+ // dash movement
addFrame(h.StartTime - timeAvailable + 1, lastPosition, true);
addFrame(h.StartTime - timeAvailable + timeAtDashSpeed, midPosition);
addFrame(h.StartTime, h.X);
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
index afde1c9521..aac77c9c1c 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
@@ -41,8 +41,6 @@ namespace osu.Game.Rulesets.Mania.Tests
AccentColour = Color4.OrangeRed,
Clock = new FramedClock(new StopwatchClock()), // No scroll
});
-
- AddStep("change direction", () => ((ScrollingTestContainer)HitObjectContainer).Flip());
}
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
new file mode 100644
index 0000000000..48159c817d
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
@@ -0,0 +1,221 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Edit;
+using osu.Game.Rulesets.Mania.Edit.Blueprints;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestSceneManiaHitObjectComposer : EditorClockTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(ManiaBlueprintContainer)
+ };
+
+ private TestComposer composer;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ BeatDivisor.Value = 8;
+ Clock.Seek(0);
+
+ Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both };
+ });
+
+ [Test]
+ public void TestDragOffscreenSelectionVerticallyUpScroll()
+ {
+ DrawableHitObject lastObject = null;
+ Vector2 originalPosition = Vector2.Zero;
+
+ setScrollStep(ScrollingDirection.Up);
+
+ AddStep("seek to last object", () =>
+ {
+ lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
+ Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
+ });
+
+ AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
+
+ AddStep("click last object", () =>
+ {
+ originalPosition = lastObject.DrawPosition;
+
+ InputManager.MoveMouseTo(lastObject);
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("move mouse downwards", () =>
+ {
+ InputManager.MoveMouseTo(lastObject, new Vector2(0, 20));
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
+ AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0));
+ AddAssert("hitobjects moved downwards", () => lastObject.DrawPosition.Y - originalPosition.Y > 0);
+ AddAssert("hitobjects not moved too far", () => lastObject.DrawPosition.Y - originalPosition.Y < 50);
+ }
+
+ [Test]
+ public void TestDragOffscreenSelectionVerticallyDownScroll()
+ {
+ DrawableHitObject lastObject = null;
+ Vector2 originalPosition = Vector2.Zero;
+
+ setScrollStep(ScrollingDirection.Down);
+
+ AddStep("seek to last object", () =>
+ {
+ lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
+ Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
+ });
+
+ AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
+
+ AddStep("click last object", () =>
+ {
+ originalPosition = lastObject.DrawPosition;
+
+ InputManager.MoveMouseTo(lastObject);
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("move mouse upwards", () =>
+ {
+ InputManager.MoveMouseTo(lastObject, new Vector2(0, -20));
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
+ AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0));
+ AddAssert("hitobjects moved upwards", () => originalPosition.Y - lastObject.DrawPosition.Y > 0);
+ AddAssert("hitobjects not moved too far", () => originalPosition.Y - lastObject.DrawPosition.Y < 50);
+ }
+
+ [Test]
+ public void TestDragOffscreenSelectionHorizontally()
+ {
+ DrawableHitObject lastObject = null;
+ Vector2 originalPosition = Vector2.Zero;
+
+ setScrollStep(ScrollingDirection.Down);
+
+ AddStep("seek to last object", () =>
+ {
+ lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
+ Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
+ });
+
+ AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
+
+ AddStep("click last object", () =>
+ {
+ originalPosition = lastObject.DrawPosition;
+
+ InputManager.MoveMouseTo(lastObject);
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("move mouse right", () =>
+ {
+ var firstColumn = composer.Composer.Playfield.GetColumn(0);
+ var secondColumn = composer.Composer.Playfield.GetColumn(1);
+
+ InputManager.MoveMouseTo(lastObject, new Vector2(secondColumn.ScreenSpaceDrawQuad.Centre.X - firstColumn.ScreenSpaceDrawQuad.Centre.X + 1, 0));
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
+ AddAssert("hitobjects moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 1));
+
+ // Todo: They'll move vertically by the height of a note since there's no snapping and the selection point is the middle of the note.
+ AddAssert("hitobjects not moved vertically", () => lastObject.DrawPosition.Y - originalPosition.Y <= DefaultNotePiece.NOTE_HEIGHT);
+ }
+
+ [Test]
+ public void TestDragHoldNoteSelectionVertically()
+ {
+ setScrollStep(ScrollingDirection.Down);
+
+ AddStep("setup beatmap", () =>
+ {
+ composer.EditorBeatmap.Clear();
+ composer.EditorBeatmap.Add(new HoldNote
+ {
+ Column = 1,
+ EndTime = 200
+ });
+ });
+
+ DrawableHoldNote holdNote = null;
+
+ AddStep("grab hold note", () =>
+ {
+ holdNote = this.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(holdNote);
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("move drag upwards", () =>
+ {
+ InputManager.MoveMouseTo(holdNote, new Vector2(0, -100));
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
+ AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft));
+ AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
+
+ AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
+ AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
+ }
+
+ private void setScrollStep(ScrollingDirection direction)
+ => AddStep($"set scroll direction = {direction}", () => ((Bindable)composer.Composer.ScrollingInfo.Direction).Value = direction);
+
+ private class TestComposer : CompositeDrawable
+ {
+ [Cached(typeof(EditorBeatmap))]
+ [Cached(typeof(IBeatSnapProvider))]
+ public readonly EditorBeatmap EditorBeatmap;
+
+ public readonly ManiaHitObjectComposer Composer;
+
+ public TestComposer()
+ {
+ InternalChildren = new Drawable[]
+ {
+ EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }))
+ {
+ BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }
+ },
+ Composer = new ManiaHitObjectComposer(new ManiaRuleset())
+ };
+
+ for (int i = 0; i < 10; i++)
+ EditorBeatmap.Add(new Note { StartTime = 100 * i });
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs
index d7b539a2a0..2d97e61aa5 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs
@@ -1,17 +1,59 @@
// 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.Extensions.IEnumerableExtensions;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests
{
public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
{
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ this.ChildrenOfType().ForEach(c => c.Clear());
+
+ ResetPlacement();
+
+ ((ScrollingTestContainer)HitObjectContainer).Direction = ScrollingDirection.Down;
+ });
+
+ [Test]
+ public void TestPlaceBeforeCurrentTimeDownwards()
+ {
+ AddStep("move mouse before current time", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single().ScreenSpaceDrawQuad.BottomLeft - new Vector2(0, 10)));
+
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("note start time < 0", () => getNote().StartTime < 0);
+ }
+
+ [Test]
+ public void TestPlaceAfterCurrentTimeDownwards()
+ {
+ AddStep("move mouse after current time", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
+
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("note start time > 0", () => getNote().StartTime > 0);
+ }
+
+ private Note getNote() => this.ChildrenOfType().FirstOrDefault()?.HitObject;
+
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
}
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index 6855b99f28..77c871718b 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index 4187e39b43..1c8116754f 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
using osuTK;
-using osu.Game.Audio;
namespace osu.Game.Rulesets.Mania.Beatmaps
{
@@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
}
}
- public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition || h is ManiaHitObject);
+ public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
protected override Beatmap ConvertBeatmap(IBeatmap original)
{
@@ -239,8 +238,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
StartTime = HitObject.StartTime,
Duration = endTimeData.Duration,
Column = column,
- Head = { Samples = sampleInfoListAt(HitObject.StartTime) },
- Tail = { Samples = sampleInfoListAt(endTimeData.EndTime) },
+ Samples = HitObject.Samples,
+ NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
});
}
else if (HitObject is IHasXPosition)
@@ -255,22 +254,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return pattern;
}
-
- ///
- /// Retrieves the sample info list at a point in time.
- ///
- /// The time to retrieve the sample info list from.
- ///
- private IList sampleInfoListAt(double time)
- {
- if (!(HitObject is IHasCurve curveData))
- return HitObject.Samples;
-
- double segmentTime = (curveData.EndTime - HitObject.StartTime) / curveData.SpanCount();
-
- int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
- return curveData.NodeSamples[index];
- }
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 315ef96e49..d8d5b67c0e 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -505,16 +505,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
}
else
{
- var holdNote = new HoldNote
+ newObject = new HoldNote
{
StartTime = startTime,
- Column = column,
Duration = endTime - startTime,
- Head = { Samples = sampleInfoListAt(startTime) },
- Tail = { Samples = sampleInfoListAt(endTime) }
+ Column = column,
+ Samples = HitObject.Samples,
+ NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
};
-
- newObject = holdNote;
}
pattern.Add(newObject);
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
index b3be08e1f7..907bed0d65 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
@@ -64,21 +64,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (holdNote)
{
- var hold = new HoldNote
+ newObject = new HoldNote
{
StartTime = HitObject.StartTime,
+ Duration = endTime - HitObject.StartTime,
Column = column,
- Duration = endTime - HitObject.StartTime
+ Samples = HitObject.Samples,
+ NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
};
-
- if (hold.Head.Samples == null)
- hold.Head.Samples = new List();
-
- hold.Head.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL });
-
- hold.Tail.Samples = HitObject.Samples;
-
- newObject = hold;
}
else
{
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
index 7bbde400ea..b3dd392202 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osuTK;
@@ -46,6 +47,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
bodyPiece.Height = (bottomPosition - topPosition).Y;
}
+ protected override void OnMouseUp(MouseUpEvent e)
+ {
+ base.OnMouseUp(e);
+ EndPlacement(true);
+ }
+
private double originalStartTime;
public override void UpdatePosition(Vector2 screenSpacePosition)
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index d569d68b59..43d43ef252 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -76,5 +76,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
}
public override Quad SelectionQuad => ScreenSpaceDrawQuad;
+
+ public override Vector2 SelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre;
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
index 6ddf212266..400abb6380 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
@@ -50,16 +50,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return base.OnMouseDown(e);
HitObject.Column = Column.Index;
- BeginPlacement(TimeAt(e.ScreenSpaceMousePosition));
+ BeginPlacement(TimeAt(e.ScreenSpaceMousePosition), true);
return true;
}
- protected override void OnMouseUp(MouseUpEvent e)
- {
- EndPlacement(true);
- base.OnMouseUp(e);
- }
-
public override void UpdatePosition(Vector2 screenSpacePosition)
{
if (!PlacementActive)
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
index 9f57160f99..b8574b804e 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
@@ -3,8 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Input.Events;
-using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
@@ -15,13 +13,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public class ManiaSelectionBlueprint : OverlaySelectionBlueprint
{
- public Vector2 ScreenSpaceDragPosition { get; private set; }
- public Vector2 DragPosition { get; private set; }
-
public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject;
- protected IClock EditorClock { get; private set; }
-
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
@@ -34,12 +27,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
RelativeSizeAxes = Axes.None;
}
- [BackgroundDependencyLoader]
- private void load(IAdjustableClock clock)
- {
- EditorClock = clock;
- }
-
protected override void Update()
{
base.Update();
@@ -47,22 +34,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
Position = Parent.ToLocalSpace(DrawableObject.ToScreenSpace(Vector2.Zero));
}
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- ScreenSpaceDragPosition = e.ScreenSpaceMousePosition;
- DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition);
-
- return base.OnMouseDown(e);
- }
-
- protected override void OnDrag(DragEvent e)
- {
- base.OnDrag(e);
-
- ScreenSpaceDragPosition = e.ScreenSpaceMousePosition;
- DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition);
- }
-
public override void Show()
{
DrawableObject.AlwaysAlive = true;
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
index 32c6a6fd07..2b7b383dbe 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
@@ -26,5 +27,15 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
Width = SnappedWidth;
Position = SnappedMousePosition;
}
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ base.OnMouseDown(e);
+
+ // Place the note immediately.
+ EndPlacement(true);
+
+ return true;
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
index d744036b4c..cea27498c3 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
@@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mania.Edit
return base.CreateBlueprintFor(hitObject);
}
+
+ protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler();
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
index 62b609610f..dfa933baad 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -37,7 +38,33 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
- public int TotalColumns => ((ManiaPlayfield)drawableRuleset.Playfield).TotalColumns;
+ public ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield);
+
+ public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
+
+ public int TotalColumns => Playfield.TotalColumns;
+
+ public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time)
+ {
+ var hoc = Playfield.GetColumn(0).HitObjectContainer;
+
+ float targetPosition = hoc.ToLocalSpace(ToScreenSpace(position)).Y;
+
+ if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down)
+ {
+ // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time.
+ // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position,
+ // so when scrolling downwards the coordinates need to be flipped.
+ targetPosition = hoc.DrawHeight - targetPosition;
+ }
+
+ double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition,
+ EditorClock.CurrentTime,
+ drawableRuleset.ScrollingInfo.TimeRange.Value,
+ hoc.DrawHeight);
+
+ return base.GetSnappedPosition(position, targetTime);
+ }
protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
{
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 9069a636a8..55245198c8 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -4,11 +4,8 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Timing;
-using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
@@ -22,85 +19,16 @@ namespace osu.Game.Rulesets.Mania.Edit
[Resolved]
private IManiaHitObjectComposer composer { get; set; }
- private IClock editorClock;
-
- [BackgroundDependencyLoader]
- private void load(IAdjustableClock clock)
- {
- editorClock = clock;
- }
-
public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
- adjustOrigins(maniaBlueprint);
- performDragMovement(moveEvent);
performColumnMovement(lastColumn, moveEvent);
return true;
}
- ///
- /// Ensures that the position of hitobjects remains centred to the mouse position.
- /// E.g. The hitobject position will change if the editor scrolls while a hitobject is dragged.
- ///
- /// The that received the drag event.
- private void adjustOrigins(ManiaSelectionBlueprint reference)
- {
- var referenceParent = (HitObjectContainer)reference.DrawableObject.Parent;
-
- float offsetFromReferenceOrigin = reference.DragPosition.Y - reference.DrawableObject.OriginPosition.Y;
- float targetPosition = referenceParent.ToLocalSpace(reference.ScreenSpaceDragPosition).Y - offsetFromReferenceOrigin;
-
- // Flip the vertical coordinate space when scrolling downwards
- if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- targetPosition -= referenceParent.DrawHeight;
-
- float movementDelta = targetPosition - reference.DrawableObject.Position.Y;
-
- foreach (var b in SelectedBlueprints.OfType())
- b.DrawableObject.Y += movementDelta;
- }
-
- private void performDragMovement(MoveSelectionEvent moveEvent)
- {
- float delta = moveEvent.InstantDelta.Y;
-
- // When scrolling downwards the anchor position is at the bottom of the screen, however the movement event assumes the anchor is at the top of the screen.
- // This causes the delta to assume a positive hitobject position, and which can be corrected for by subtracting the parent height.
- if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- delta -= moveEvent.Blueprint.Parent.DrawHeight; // todo: probably wrong
-
- foreach (var selectionBlueprint in SelectedBlueprints)
- {
- var b = (OverlaySelectionBlueprint)selectionBlueprint;
-
- var hitObject = b.DrawableObject;
- var objectParent = (HitObjectContainer)hitObject.Parent;
-
- // StartTime could be used to adjust the position if only one movement event was received per frame.
- // However this is not the case and ScrollingHitObjectContainer performs movement in UpdateAfterChildren() so the position must also be updated to be valid for further movement events
- hitObject.Y += delta;
-
- float targetPosition = hitObject.Position.Y;
-
- // The scrolling algorithm always assumes an anchor at the top of the screen, so the position must be flipped when scrolling downwards to reflect a top anchor
- if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- targetPosition = -targetPosition;
-
- objectParent.Remove(hitObject);
-
- hitObject.HitObject.StartTime = scrollingInfo.Algorithm.TimeAt(targetPosition,
- editorClock.CurrentTime,
- scrollingInfo.TimeRange.Value,
- objectParent.DrawHeight);
-
- objectParent.Add(hitObject);
- }
- }
-
private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
{
var currentColumn = composer.ColumnAt(moveEvent.ScreenSpacePosition);
diff --git a/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs
deleted file mode 100644
index 433db79ae0..0000000000
--- a/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Objects.Drawables;
-
-namespace osu.Game.Rulesets.Mania.Edit.Masks
-{
- public abstract class ManiaSelectionBlueprint : OverlaySelectionBlueprint
- {
- protected ManiaSelectionBlueprint(DrawableHitObject drawableObject)
- : base(drawableObject)
- {
- RelativeSizeAxes = Axes.None;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index a9ef661aaa..2262bd2b7d 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -51,7 +51,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
AddRangeInternal(new[]
{
- bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece())
+ bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
+ {
+ RelativeSizeAxes = Axes.Both
+ })
{
RelativeSizeAxes = Axes.X
},
@@ -127,6 +130,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
}
+ public override void PlaySamples()
+ {
+ // Samples are played by the head/tail notes.
+ }
+
protected override void Update()
{
base.Update();
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index 88888001b4..a44d8b09aa 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -13,11 +13,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
public abstract class DrawableManiaHitObject : DrawableHitObject
{
- ///
- /// Whether this should always remain alive.
- ///
- internal bool AlwaysAlive;
-
///
/// The which causes this to be hit.
///
@@ -54,7 +49,62 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Direction.BindValueChanged(OnDirectionChanged, true);
}
- protected override bool ShouldBeAlive => AlwaysAlive || base.ShouldBeAlive;
+ private double computedLifetimeStart;
+
+ public override double LifetimeStart
+ {
+ get => base.LifetimeStart;
+ set
+ {
+ computedLifetimeStart = value;
+
+ if (!AlwaysAlive)
+ base.LifetimeStart = value;
+ }
+ }
+
+ private double computedLifetimeEnd;
+
+ public override double LifetimeEnd
+ {
+ get => base.LifetimeEnd;
+ set
+ {
+ computedLifetimeEnd = value;
+
+ if (!AlwaysAlive)
+ base.LifetimeEnd = value;
+ }
+ }
+
+ private bool alwaysAlive;
+
+ ///
+ /// Whether this should always remain alive.
+ ///
+ internal bool AlwaysAlive
+ {
+ get => alwaysAlive;
+ set
+ {
+ if (alwaysAlive == value)
+ return;
+
+ alwaysAlive = value;
+
+ if (value)
+ {
+ // Set the base lifetimes directly, to avoid mangling the computed lifetimes
+ base.LifetimeStart = double.MinValue;
+ base.LifetimeEnd = double.MaxValue;
+ }
+ else
+ {
+ LifetimeStart = computedLifetimeStart;
+ LifetimeEnd = computedLifetimeEnd;
+ }
+ }
+ }
protected virtual void OnDirectionChanged(ValueChangedEvent e)
{
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
index 0ee0a14df3..bc4a095395 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
@@ -34,7 +34,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
public DefaultBodyPiece()
{
- RelativeSizeAxes = Axes.Both;
Blending = BlendingParameters.Additive;
AddLayout(subtractionCache);
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index 049bf55f90..eea2c31260 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
@@ -28,7 +30,9 @@ namespace osu.Game.Rulesets.Mania.Objects
set
{
duration = value;
- Tail.StartTime = EndTime;
+
+ if (Tail != null)
+ Tail.StartTime = EndTime;
}
}
@@ -38,8 +42,12 @@ namespace osu.Game.Rulesets.Mania.Objects
set
{
base.StartTime = value;
- Head.StartTime = value;
- Tail.StartTime = EndTime;
+
+ if (Head != null)
+ Head.StartTime = value;
+
+ if (Tail != null)
+ Tail.StartTime = EndTime;
}
}
@@ -49,20 +57,26 @@ namespace osu.Game.Rulesets.Mania.Objects
set
{
base.Column = value;
- Head.Column = value;
- Tail.Column = value;
+
+ if (Head != null)
+ Head.Column = value;
+
+ if (Tail != null)
+ Tail.Column = value;
}
}
+ public List> NodeSamples { get; set; }
+
///
/// The head note of the hold.
///
- public readonly Note Head = new Note();
+ public Note Head { get; private set; }
///
/// The tail note of the hold.
///
- public readonly TailNote Tail = new TailNote();
+ public TailNote Tail { get; private set; }
///
/// The time between ticks of this hold.
@@ -83,8 +97,19 @@ namespace osu.Game.Rulesets.Mania.Objects
createTicks();
- AddNested(Head);
- AddNested(Tail);
+ AddNested(Head = new Note
+ {
+ StartTime = StartTime,
+ Column = Column,
+ Samples = getNodeSamples(0),
+ });
+
+ AddNested(Tail = new TailNote
+ {
+ StartTime = EndTime,
+ Column = Column,
+ Samples = getNodeSamples((NodeSamples?.Count - 1) ?? 1),
+ });
}
private void createTicks()
@@ -105,5 +130,8 @@ namespace osu.Game.Rulesets.Mania.Objects
public override Judgement CreateJudgement() => new IgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+
+ private IList getNodeSamples(int nodeIndex) =>
+ nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
index 995e1516cb..27bf50493d 100644
--- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
@@ -5,11 +5,12 @@ using osu.Framework.Bindables;
using osu.Game.Rulesets.Mania.Objects.Types;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects
{
- public abstract class ManiaHitObject : HitObject, IHasColumn
+ public abstract class ManiaHitObject : HitObject, IHasColumn, IHasXPosition
{
public readonly Bindable ColumnBindable = new Bindable();
@@ -20,5 +21,11 @@ namespace osu.Game.Rulesets.Mania.Objects
}
protected override HitWindows CreateHitWindows() => new ManiaHitWindows();
+
+ #region LegacyBeatmapEncoder
+
+ float IHasXPosition.X => Column;
+
+ #endregion
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index 14cad39b04..f3f843f366 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
@@ -48,6 +50,10 @@ namespace osu.Game.Rulesets.Mania.UI
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
private readonly Bindable configDirection = new Bindable();
+ private readonly Bindable configTimeRange = new BindableDouble();
+
+ // Stores the current speed adjustment active in gameplay.
+ private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
: base(ruleset, beatmap, mods)
@@ -58,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.UI
[BackgroundDependencyLoader]
private void load()
{
+ foreach (var mod in Mods.OfType())
+ mod.ApplyToTrack(speedAdjustmentTrack);
+
bool isForCurrentRuleset = Beatmap.BeatmapInfo.Ruleset.Equals(Ruleset.RulesetInfo);
foreach (var p in ControlPoints)
@@ -76,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.UI
Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection);
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
- Config.BindWith(ManiaRulesetSetting.ScrollTime, TimeRange);
+ Config.BindWith(ManiaRulesetSetting.ScrollTime, configTimeRange);
}
protected override void AdjustScrollSpeed(int amount)
@@ -86,10 +95,19 @@ namespace osu.Game.Rulesets.Mania.UI
private double relativeTimeRange
{
- get => MAX_TIME_RANGE / TimeRange.Value;
- set => TimeRange.Value = MAX_TIME_RANGE / value;
+ get => MAX_TIME_RANGE / configTimeRange.Value;
+ set => configTimeRange.Value = MAX_TIME_RANGE / value;
}
+ protected override void Update()
+ {
+ base.Update();
+
+ updateTimeRange();
+ }
+
+ private void updateTimeRange() => TimeRange.Value = configTimeRange.Value * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
+
///
/// Retrieves the column that intersects a screen-space position.
///
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 2dec468654..1af7d06998 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.UI
{
foreach (var column in stage.Columns)
{
- if (column.ReceivePositionalInputAt(screenSpacePosition))
+ if (column.ReceivePositionalInputAt(new Vector2(screenSpacePosition.X, column.ScreenSpaceDrawQuad.Centre.Y)))
{
found = column;
break;
@@ -87,6 +87,31 @@ namespace osu.Game.Rulesets.Mania.UI
return found;
}
+ ///
+ /// Retrieves a by index.
+ ///
+ /// The index of the column.
+ /// The corresponding to the given index.
+ /// If is less than 0 or greater than .
+ public Column GetColumn(int index)
+ {
+ if (index < 0 || index > TotalColumns - 1)
+ throw new ArgumentOutOfRangeException(nameof(index));
+
+ foreach (var stage in stages)
+ {
+ if (index >= stage.Columns.Count)
+ {
+ index -= stage.Columns.Count;
+ continue;
+ }
+
+ return stage.Columns[index];
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
///
/// Retrieves the total amount of columns across all stages in this playfield.
///
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
index a6c3be7e5a..c3b4d2625e 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
@@ -399,7 +399,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
- DefaultsApplied += () =>
+ DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();
TailCircle.HitWindows = new TestHitWindows();
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs
index 9fc479953e..fe9973f4d8 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs
@@ -259,6 +259,23 @@ namespace osu.Game.Rulesets.Osu.Tests
assertControlPointType(2, PathType.PerfectCurve);
}
+ [Test]
+ public void TestBeginPlacementWithoutReleasingMouse()
+ {
+ addMovementStep(new Vector2(200));
+ AddStep("press left button", () => InputManager.PressButton(MouseButton.Left));
+
+ addMovementStep(new Vector2(400, 200));
+ AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left));
+
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertLength(200);
+ assertControlPointCount(2);
+ assertControlPointType(0, PathType.Linear);
+ }
+
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
private void addClickStep(MouseButton button)
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 217707b180..2fcfa1deb7 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
index 3a829f72fa..f51f04bf87 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
double stackThreshold = objectN.TimePreempt * beatmap.BeatmapInfo.StackLeniency;
if (objectN.StartTime - endTime > stackThreshold)
- //We are no longer within stacking range of the next object.
+ // We are no longer within stacking range of the next object.
break;
if (Vector2Extensions.Distance(stackBaseObject.Position, objectN.Position) < stack_distance
@@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
}
}
- //Reverse pass for stack calculation.
+ // Reverse pass for stack calculation.
int extendedStartIndex = startIndex;
for (int i = extendedEndIndex; i > startIndex; i--)
@@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
double endTime = objectN.GetEndTime();
if (objectI.StartTime - endTime > stackThreshold)
- //We are no longer within stacking range of the previous object.
+ // We are no longer within stacking range of the previous object.
break;
// HitObjects before the specified update range haven't been reset yet
@@ -145,20 +145,20 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
for (int j = n + 1; j <= i; j++)
{
- //For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above).
+ // For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above).
OsuHitObject objectJ = beatmap.HitObjects[j];
if (Vector2Extensions.Distance(objectN.EndPosition, objectJ.Position) < stack_distance)
objectJ.StackHeight -= offset;
}
- //We have hit a slider. We should restart calculation using this as the new base.
- //Breaking here will mean that the slider still has StackCount of 0, so will be handled in the i-outer-loop.
+ // We have hit a slider. We should restart calculation using this as the new base.
+ // Breaking here will mean that the slider still has StackCount of 0, so will be handled in the i-outer-loop.
break;
}
if (Vector2Extensions.Distance(objectN.Position, objectI.Position) < stack_distance)
{
- //Keep processing as if there are no sliders. If we come across a slider, this gets cancelled out.
+ // Keep processing as if there are no sliders. If we come across a slider, this gets cancelled out.
//NOTE: Sliders with start positions stacking are a special case that is also handled here.
objectN.StackHeight = objectI.StackHeight + 1;
@@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
if (objectN is Spinner) continue;
if (objectI.StartTime - objectN.StartTime > stackThreshold)
- //We are no longer within stacking range of the previous object.
+ // We are no longer within stacking range of the previous object.
break;
if (Vector2Extensions.Distance(objectN.EndPosition, objectI.Position) < stack_distance)
@@ -221,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
}
else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance)
{
- //Case for sliders - bump notes down and right, rather than up and left.
+ // Case for sliders - bump notes down and right, rather than up and left.
sliderStack++;
beatmap.HitObjects[j].StackHeight -= sliderStack;
startTime = beatmap.HitObjects[j].GetEndTime();
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
index 407f5f540e..dad199715e 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
@@ -6,6 +6,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
{
@@ -28,16 +29,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
circlePiece.UpdateFrom(HitObject);
}
- protected override bool OnClick(ClickEvent e)
+ protected override bool OnMouseDown(MouseDownEvent e)
{
- EndPlacement(true);
- return true;
+ if (e.Button == MouseButton.Left)
+ {
+ EndPlacement(true);
+ return true;
+ }
+
+ return base.OnMouseDown(e);
}
- public override void UpdatePosition(Vector2 screenSpacePosition)
- {
- BeginPlacement();
- HitObject.Position = ToLocalSpace(screenSpacePosition);
- }
+ public override void UpdatePosition(Vector2 screenSpacePosition) => HitObject.Position = ToLocalSpace(screenSpacePosition);
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
index fed149b5c5..d0c1eb5317 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -28,7 +28,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public Action RequestSelection;
public readonly BindableBool IsSelected = new BindableBool();
-
public readonly PathControlPoint ControlPoint;
private readonly Slider slider;
@@ -146,6 +145,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override bool OnDragStart(DragStartEvent e)
{
+ if (RequestSelection == null)
+ return false;
+
if (e.Button == MouseButton.Left)
{
changeHandler?.BeginChange();
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index 9af972dbce..ac30f5a762 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -82,8 +82,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
- protected override bool OnClick(ClickEvent e)
+ protected override bool OnMouseDown(MouseDownEvent e)
{
+ if (e.Button != MouseButton.Left)
+ return base.OnMouseDown(e);
+
switch (state)
{
case PlacementState.Initial:
@@ -91,9 +94,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
break;
case PlacementState.Body:
- if (e.Button != MouseButton.Left)
- break;
-
if (canPlaceNewControlPoint(out var lastPoint))
{
// Place a new point by detatching the current cursor.
@@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
currentSegmentLength = 1;
}
- return true;
+ break;
}
return true;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
index 44dba7715a..5e80d08667 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Mods
Vector2 originalPosition = drawable.Position;
Vector2 appearOffset = new Vector2(MathF.Cos(theta), MathF.Sin(theta)) * appearDistance;
- //the - 1 and + 1 prevents the hit objects to appear in the wrong position.
+ // the - 1 and + 1 prevents the hit objects to appear in the wrong position.
double appearTime = hitObject.StartTime - hitObject.TimePreempt - 1;
double moveDuration = hitObject.TimePreempt + 1;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
index 6f09bbcd57..8a0ef22c4a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
private void bindEvents(DrawableOsuHitObject drawableObject)
{
drawableObject.HitObject.PositionBindable.BindValueChanged(_ => scheduleRefresh());
- drawableObject.HitObject.DefaultsApplied += scheduleRefresh;
+ drawableObject.HitObject.DefaultsApplied += _ => scheduleRefresh();
}
private void scheduleRefresh()
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs
index e364c96426..cb3787a493 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
using (BeginDelayedSequence(flash_in, true))
{
- //after the flash, we can hide some elements that were behind it
+ // after the flash, we can hide some elements that were behind it
ring.FadeOut();
circle.FadeOut();
number.FadeOut();
diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs
new file mode 100644
index 0000000000..1db07b3244
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ internal class DrawableTestHit : DrawableTaikoHitObject
+ {
+ private readonly HitResult type;
+
+ public DrawableTestHit(Hit hit, HitResult type = HitResult.Great)
+ : base(hit)
+ {
+ this.type = type;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Result.Type = type;
+ }
+
+ public override bool OnPressed(TaikoAction action) => false;
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-bar-right@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-bar-right@2x.png
new file mode 100644
index 0000000000..5ca8a40d88
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-bar-right@2x.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-barline@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-barline@2x.png
new file mode 100644
index 0000000000..3e44f33095
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-barline@2x.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
new file mode 100644
index 0000000000..70493aa69a
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
@@ -0,0 +1,111 @@
+// 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 NUnit.Framework;
+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.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Skinning;
+using osu.Game.Rulesets.Taiko.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneDrawableBarLine : TaikoSkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
+ {
+ typeof(DrawableBarLine),
+ typeof(LegacyBarLine),
+ typeof(BarLine),
+ }).ToList();
+
+ [Cached(typeof(IScrollingInfo))]
+ private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
+ {
+ Direction = { Value = ScrollingDirection.Left },
+ TimeRange = { Value = 5000 },
+ };
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddStep("Bar line", () => SetContents(() =>
+ {
+ ScrollingHitObjectContainer hoc;
+
+ var cont = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.8f,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new TaikoPlayfield(new ControlPointInfo()),
+ hoc = new ScrollingHitObjectContainer()
+ }
+ };
+
+ hoc.Add(new DrawableBarLine(createBarLineAtCurrentTime())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+
+ return cont;
+ }));
+
+ AddStep("Bar line (major)", () => SetContents(() =>
+ {
+ ScrollingHitObjectContainer hoc;
+
+ var cont = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.8f,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new TaikoPlayfield(new ControlPointInfo()),
+ hoc = new ScrollingHitObjectContainer()
+ }
+ };
+
+ hoc.Add(new DrawableBarLineMajor(createBarLineAtCurrentTime(true))
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+
+ return cont;
+ }));
+ }
+
+ private BarLine createBarLineAtCurrentTime(bool major = false)
+ {
+ var barline = new BarLine
+ {
+ Major = major,
+ StartTime = Time.Current + 2000,
+ };
+
+ var cpi = new ControlPointInfo();
+ cpi.Add(0, new TimingControlPoint { BeatLength = 500 });
+
+ barline.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ return barline;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
index 6d6da1fb5b..6a3c98a514 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
@@ -21,8 +21,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(DrawableHit),
- typeof(DrawableCentreHit),
- typeof(DrawableRimHit),
typeof(LegacyHit),
typeof(LegacyCirclePiece),
}).ToList();
@@ -30,25 +28,25 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- AddStep("Centre hit", () => SetContents(() => new DrawableCentreHit(createHitAtCurrentTime())
+ AddStep("Centre hit", () => SetContents(() => new DrawableHit(createHitAtCurrentTime())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}));
- AddStep("Centre hit (strong)", () => SetContents(() => new DrawableCentreHit(createHitAtCurrentTime(true))
+ AddStep("Centre hit (strong)", () => SetContents(() => new DrawableHit(createHitAtCurrentTime(true))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}));
- AddStep("Rim hit", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime())
+ AddStep("Rim hit", () => SetContents(() => new DrawableHit(createHitAtCurrentTime())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}));
- AddStep("Rim hit (strong)", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime(true))
+ AddStep("Rim hit (strong)", () => SetContents(() => new DrawableHit(createHitAtCurrentTime(true))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
new file mode 100644
index 0000000000..791c438c94
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
@@ -0,0 +1,58 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+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.Skinning;
+using osu.Game.Rulesets.Taiko.UI;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneHitExplosion : TaikoSkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
+ {
+ typeof(HitExplosion),
+ typeof(LegacyHitExplosion),
+ typeof(DefaultHitExplosion),
+ }).ToList();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddStep("Great", () => SetContents(() => getContentFor(HitResult.Great)));
+ AddStep("Good", () => SetContents(() => getContentFor(HitResult.Good)));
+ AddStep("Miss", () => SetContents(() => getContentFor(HitResult.Miss)));
+ }
+
+ private Drawable getContentFor(HitResult type)
+ {
+ DrawableTaikoHitObject hit;
+
+ return new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ hit = createHit(type),
+ new HitExplosion(hit)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ }
+ };
+ }
+
+ private DrawableTaikoHitObject createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type);
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
index 730eed0e0f..ae5dd1e622 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
@@ -5,8 +5,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI.Scrolling;
@@ -18,8 +21,9 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
{
- typeof(HitTarget),
- typeof(LegacyHitTarget),
+ typeof(TaikoHitTarget),
+ typeof(TaikoLegacyHitTarget),
+ typeof(PlayfieldBackgroundRight),
}).ToList();
[Cached(typeof(IScrollingInfo))]
@@ -31,12 +35,30 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
public TestSceneTaikoPlayfield()
{
+ TaikoBeatmap beatmap;
+ bool kiai = false;
+
+ AddStep("set beatmap", () =>
+ {
+ Beatmap.Value = CreateWorkingBeatmap(beatmap = new TaikoBeatmap());
+
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 });
+
+ Beatmap.Value.Track.Start();
+ });
+
AddStep("Load playfield", () => SetContents(() => new TaikoPlayfield(new ControlPointInfo())
{
- Height = 0.4f,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}));
+
+ AddRepeatStep("change height", () => this.ChildrenOfType().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50);
+
+ AddStep("Toggle kiai", () =>
+ {
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new EffectControlPoint { KiaiMode = (kiai = !kiai) });
+ });
}
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
index c2ca578dfa..44452d70c1 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[TestFixture]
public class TestSceneHits : OsuTestScene
{
- private const double default_duration = 1000;
+ private const double default_duration = 3000;
private const float scroll_time = 1000;
protected override double TimePerAction => default_duration * 2;
@@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddStep("Miss :(", addMissJudgement);
AddStep("DrumRoll", () => addDrumRoll(false));
AddStep("Strong DrumRoll", () => addDrumRoll(true));
+ AddStep("Kiai DrumRoll", () => addDrumRoll(true, kiai: true));
AddStep("Swell", () => addSwell());
AddStep("Centre", () => addCentreHit(false));
AddStep("Strong Centre", () => addCentreHit(true));
@@ -148,6 +149,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
+ Add(h);
+
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
}
@@ -163,6 +166,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
+ Add(h);
+
((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 });
}
@@ -192,7 +197,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
drawableRuleset.Playfield.Add(new DrawableSwell(swell));
}
- private void addDrumRoll(bool strong, double duration = default_duration)
+ private void addDrumRoll(bool strong, double duration = default_duration, bool kiai = false)
{
addBarLine(true);
addBarLine(true, scroll_time + duration);
@@ -202,9 +207,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong,
Duration = duration,
+ TickRate = 8,
};
- d.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ var cpi = new ControlPointInfo();
+ cpi.Add(-10000, new EffectControlPoint { KiaiMode = kiai });
+
+ d.ApplyDefaults(cpi, new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableDrumRoll(d));
}
@@ -219,7 +228,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- drawableRuleset.Playfield.Add(new DrawableCentreHit(h));
+ drawableRuleset.Playfield.Add(new DrawableHit(h));
}
private void addRimHit(bool strong)
@@ -232,7 +241,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- drawableRuleset.Playfield.Add(new DrawableRimHit(h));
+ drawableRuleset.Playfield.Add(new DrawableHit(h));
}
private class TestStrongNestedHit : DrawableStrongNestedHit
@@ -244,13 +253,5 @@ namespace osu.Game.Rulesets.Taiko.Tests
public override bool OnPressed(TaikoAction action) => false;
}
-
- private class DrawableTestHit : DrawableHitObject
- {
- public DrawableTestHit(TaikoHitObject hitObject)
- : base(hitObject)
- {
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index f6054a5d6f..28b8476a22 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 695ada3a00..caf645d5a2 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
/// osu! is generally slower than taiko, so a factor is added to increase
/// speed. This must be used everywhere slider length or beat length is used.
///
- private const float legacy_velocity_multiplier = 1.4f;
+ public const float LEGACY_VELOCITY_MULTIPLIER = 1.4f;
///
/// Because swells are easier in taiko than spinners are in osu!,
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
// Rewrite the beatmap info to add the slider velocity multiplier
original.BeatmapInfo = original.BeatmapInfo.Clone();
original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone();
- original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= legacy_velocity_multiplier;
+ original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= LEGACY_VELOCITY_MULTIPLIER;
Beatmap converted = base.ConvertBeatmap(original);
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double speedAdjustedBeatLength = timingPoint.BeatLength / speedAdjustment;
// The true distance, accounting for any repeats. This ends up being the drum roll distance later
- double distance = distanceData.Distance * spans * legacy_velocity_multiplier;
+ double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER;
// The velocity of the taiko hit object - calculated as the velocity of a drum roll
double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs
index e9caabbcc8..1e08e921a6 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs
@@ -6,6 +6,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Objects;
using osuTK;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -27,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
///
/// The visual line tracker.
///
- protected Box Tracker;
+ protected SkinnableDrawable Line;
///
/// The bar line.
@@ -45,13 +46,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
RelativeSizeAxes = Axes.Y;
Width = tracker_width;
- AddInternal(Tracker = new Box
+ AddInternal(Line = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.BarLine), _ => new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ EdgeSmoothness = new Vector2(0.5f, 0),
+ })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- EdgeSmoothness = new Vector2(0.5f, 0),
- Alpha = 0.75f
+ Alpha = 0.75f,
});
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs
index 4d3a1a3f8a..62aab3524b 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
});
- Tracker.Alpha = 1f;
+ Line.Alpha = 1f;
}
protected override void LoadComplete()
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs
deleted file mode 100644
index a87da44415..0000000000
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// 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.Taiko.Objects.Drawables.Pieces;
-using osu.Game.Skinning;
-
-namespace osu.Game.Rulesets.Taiko.Objects.Drawables
-{
- public class DrawableCentreHit : DrawableHit
- {
- public override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre };
-
- public DrawableCentreHit(Hit hit)
- : base(hit)
- {
- }
-
- protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit),
- _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
index 99f48afff0..5e731e5ad6 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
@@ -121,8 +121,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return;
int countHit = NestedHitObjects.Count(o => o.IsHit);
+
if (countHit >= HitObject.RequiredGoodHits)
+ {
ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good);
+ }
else
ApplyResult(r => r.Type = HitResult.Miss);
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
index 689a7bfa64..62405cf047 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
@@ -12,14 +12,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableDrumRollTick : DrawableTaikoHitObject
{
+ ///
+ /// The hit type corresponding to the that the user pressed to hit this .
+ ///
+ public HitType JudgementType;
+
public DrawableDrumRollTick(DrumRollTick tick)
: base(tick)
{
FillMode = FillMode.Fit;
}
- public override bool DisplayResult => false;
-
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollTick),
_ => new TickPiece
{
@@ -51,7 +54,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
}
- public override bool OnPressed(TaikoAction action) => UpdateResult(true);
+ public override bool OnPressed(TaikoAction action)
+ {
+ JudgementType = action == TaikoAction.LeftRim || action == TaikoAction.RightRim ? HitType.Rim : HitType.Centre;
+ return UpdateResult(true);
+ }
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs
new file mode 100644
index 0000000000..460e760629
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs
@@ -0,0 +1,31 @@
+// 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;
+
+namespace osu.Game.Rulesets.Taiko.Objects.Drawables
+{
+ ///
+ /// A hit used specifically for drum rolls, where spawning flying hits is required.
+ ///
+ public class DrawableFlyingHit : DrawableHit
+ {
+ public DrawableFlyingHit(DrawableDrumRollTick drumRollTick)
+ : base(new IgnoreHit
+ {
+ StartTime = drumRollTick.HitObject.StartTime + drumRollTick.Result.TimeOffset,
+ IsStrong = drumRollTick.HitObject.IsStrong,
+ Type = drumRollTick.JudgementType
+ })
+ {
+ HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
index 9333e5f144..d2671eadda 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
@@ -8,31 +8,45 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
- public abstract class DrawableHit : DrawableTaikoHitObject
+ public class DrawableHit : DrawableTaikoHitObject
{
///
/// A list of keys which can result in hits for this HitObject.
///
- public abstract TaikoAction[] HitActions { get; }
+ public TaikoAction[] HitActions { get; }
///
/// The action that caused this to be hit.
///
- public TaikoAction? HitAction { get; private set; }
+ public TaikoAction? HitAction
+ {
+ get;
+ private set;
+ }
private bool validActionPressed;
private bool pressHandledThisFrame;
- protected DrawableHit(Hit hit)
+ public DrawableHit(Hit hit)
: base(hit)
{
FillMode = FillMode.Fit;
+
+ HitActions =
+ HitObject.Type == HitType.Centre
+ ? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre }
+ : new[] { TaikoAction.LeftRim, TaikoAction.RightRim };
}
+ protected override SkinnableDrawable CreateMainPiece() => HitObject.Type == HitType.Centre
+ ? new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit)
+ : new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
+
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
Debug.Assert(HitObject.HitWindows != null);
@@ -58,7 +72,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
if (pressHandledThisFrame)
return true;
-
if (Judged)
return false;
@@ -66,14 +79,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
// Only count this as handled if the new judgement is a hit
var result = UpdateResult(true);
-
if (IsHit)
HitAction = action;
// Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded
// E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note
pressHandledThisFrame = true;
-
return result;
}
@@ -81,7 +92,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
if (action == HitAction)
HitAction = null;
-
base.OnReleased(action);
}
@@ -92,8 +102,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
// The input manager processes all input prior to us updating, so this is the perfect time
// for us to remove the extra press blocking, before input is handled in the next frame
pressHandledThisFrame = false;
-
- Size = BaseSize * Parent.RelativeChildSize;
}
protected override void UpdateStateTransforms(ArmedState state)
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs
deleted file mode 100644
index f767403c65..0000000000
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// 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.Taiko.Objects.Drawables.Pieces;
-using osu.Game.Skinning;
-
-namespace osu.Game.Rulesets.Taiko.Objects.Drawables
-{
- public class DrawableRimHit : DrawableHit
- {
- public override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim };
-
- public DrawableRimHit(Hit hit)
- : base(hit)
- {
- }
-
- protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit),
- _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index aacd78f176..dc2f277e58 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -3,15 +3,20 @@
using osu.Game.Rulesets.Objects.Types;
using System;
+using System.Collections.Generic;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Judgements;
+using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects
{
- public class DrumRoll : TaikoHitObject, IHasEndTime
+ public class DrumRoll : TaikoHitObject, IHasCurve
{
///
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
@@ -26,6 +31,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
public double Duration { get; set; }
+ ///
+ /// Velocity of this .
+ ///
+ public double Velocity { get; private set; }
+
///
/// Numer of ticks per beat length.
///
@@ -54,6 +64,10 @@ namespace osu.Game.Rulesets.Taiko.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
+ DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
+
+ double scoringDistance = base_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ Velocity = scoringDistance / timingPoint.BeatLength;
tickSpacing = timingPoint.BeatLength / TickRate;
overallDifficulty = difficulty.OverallDifficulty;
@@ -93,5 +107,18 @@ namespace osu.Game.Rulesets.Taiko.Objects
public override Judgement CreateJudgement() => new TaikoDrumRollJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+
+ #region LegacyBeatmapEncoder
+
+ double IHasDistance.Distance => Duration * Velocity;
+
+ int IHasRepeats.RepeatCount { get => 0; set { } }
+
+ List> IHasRepeats.NodeSamples => new List>();
+
+ SliderPath IHasCurve.Path
+ => new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER);
+
+ #endregion
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs b/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs
new file mode 100644
index 0000000000..302f940ef4
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs
@@ -0,0 +1,12 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Judgements;
+
+namespace osu.Game.Rulesets.Taiko.Objects
+{
+ public class IgnoreHit : Hit
+ {
+ public override Judgement CreateJudgement() => new IgnoreJudgement();
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyBarLine.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyBarLine.cs
new file mode 100644
index 0000000000..7d08a21ab1
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyBarLine.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Taiko.Skinning
+{
+ public class LegacyBarLine : Sprite
+ {
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ Texture = skin.GetTexture("taiko-barline");
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ RelativeSizeAxes = Axes.Both;
+ Size = new Vector2(1, 0.88f);
+ FillMode = FillMode.Fill;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs
new file mode 100644
index 0000000000..b5ec2e8def
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+
+namespace osu.Game.Rulesets.Taiko.Skinning
+{
+ public class LegacyHitExplosion : CompositeDrawable
+ {
+ public LegacyHitExplosion(Drawable sprite)
+ {
+ InternalChild = sprite;
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ AutoSizeAxes = Axes.Both;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ const double animation_time = 120;
+
+ this.FadeInFromZero(animation_time).Then().FadeOut(animation_time * 1.5);
+
+ this.ScaleTo(0.6f)
+ .Then().ScaleTo(1.1f, animation_time * 0.8)
+ .Then().ScaleTo(0.9f, animation_time * 0.4)
+ .Then().ScaleTo(1f, animation_time * 0.2);
+
+ Expire(true);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitTarget.cs
deleted file mode 100644
index 51aea9b9ab..0000000000
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitTarget.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Skinning;
-using osuTK;
-
-namespace osu.Game.Rulesets.Taiko.Skinning
-{
- public class LegacyHitTarget : CompositeDrawable
- {
- [BackgroundDependencyLoader]
- private void load(ISkinSource skin)
- {
- RelativeSizeAxes = Axes.Both;
-
- InternalChildren = new Drawable[]
- {
- new Sprite
- {
- Texture = skin.GetTexture("approachcircle"),
- Scale = new Vector2(0.73f),
- Alpha = 0.7f,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- new Sprite
- {
- Texture = skin.GetTexture("taikobigcircle"),
- Scale = new Vector2(0.7f),
- Alpha = 0.5f,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- };
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
index c61e35692b..81d645e294 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
@@ -20,36 +20,41 @@ namespace osu.Game.Rulesets.Taiko.Skinning
{
private LegacyHalfDrum left;
private LegacyHalfDrum right;
+ private Container content;
public LegacyInputDrum()
{
- Size = new Vector2(180, 200);
+ RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
- Children = new Drawable[]
+ Child = content = new Container
{
- new Sprite
+ Size = new Vector2(180, 200),
+ Children = new Drawable[]
{
- Texture = skin.GetTexture("taiko-bar-left")
- },
- left = new LegacyHalfDrum(false)
- {
- Name = "Left Half",
- RelativeSizeAxes = Axes.Both,
- RimAction = TaikoAction.LeftRim,
- CentreAction = TaikoAction.LeftCentre
- },
- right = new LegacyHalfDrum(true)
- {
- Name = "Right Half",
- RelativeSizeAxes = Axes.Both,
- Origin = Anchor.TopRight,
- Scale = new Vector2(-1, 1),
- RimAction = TaikoAction.RightRim,
- CentreAction = TaikoAction.RightCentre
+ new Sprite
+ {
+ Texture = skin.GetTexture("taiko-bar-left")
+ },
+ left = new LegacyHalfDrum(false)
+ {
+ Name = "Left Half",
+ RelativeSizeAxes = Axes.Both,
+ RimAction = TaikoAction.LeftRim,
+ CentreAction = TaikoAction.LeftCentre
+ },
+ right = new LegacyHalfDrum(true)
+ {
+ Name = "Right Half",
+ RelativeSizeAxes = Axes.Both,
+ Origin = Anchor.TopRight,
+ Scale = new Vector2(-1, 1),
+ RimAction = TaikoAction.RightRim,
+ CentreAction = TaikoAction.RightCentre
+ }
}
};
@@ -60,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
const float ratio = 1.6f;
// because the right half is flipped, we need to position using width - position to get the true "topleft" origin position
- float negativeScaleAdjust = Width / ratio;
+ float negativeScaleAdjust = content.Width / ratio;
if (skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.1m)
{
@@ -78,6 +83,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning
}
}
+ protected override void Update()
+ {
+ base.Update();
+
+ // Relying on RelativeSizeAxes.Both + FillMode.Fit doesn't work due to the precise pixel layout requirements.
+ // This is a bit ugly but makes the non-legacy implementations a lot cleaner to implement.
+ content.Scale = new Vector2(DrawHeight / content.Size.Y);
+ }
+
///
/// A half-drum. Contains one centre and one rim hit.
///
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs
new file mode 100644
index 0000000000..e522fb7c10
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs
@@ -0,0 +1,59 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Taiko.UI;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Taiko.Skinning
+{
+ public class TaikoLegacyHitTarget : CompositeDrawable
+ {
+ private Container content;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = content = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new Sprite
+ {
+ Texture = skin.GetTexture("approachcircle"),
+ Scale = new Vector2(0.73f),
+ Alpha = 0.47f, // eyeballed to match stable
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ new Sprite
+ {
+ Texture = skin.GetTexture("taikobigcircle"),
+ Scale = new Vector2(0.7f),
+ Alpha = 0.22f, // eyeballed to match stable
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ }
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ // Relying on RelativeSizeAxes.Both + FillMode.Fit doesn't work due to the precise pixel layout requirements.
+ // This is a bit ugly but makes the non-legacy implementations a lot cleaner to implement.
+ content.Scale = new Vector2(DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs
new file mode 100644
index 0000000000..7508c75231
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs
@@ -0,0 +1,57 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Audio.Track;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.Containers;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Taiko.Skinning
+{
+ public class TaikoLegacyPlayfieldBackgroundRight : BeatSyncedContainer
+ {
+ private Sprite kiai;
+
+ private bool kiaiDisplayed;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new Sprite
+ {
+ Texture = skin.GetTexture("taiko-bar-right"),
+ RelativeSizeAxes = Axes.Both,
+ Size = Vector2.One,
+ },
+ kiai = new Sprite
+ {
+ Texture = skin.GetTexture("taiko-bar-right-glow"),
+ RelativeSizeAxes = Axes.Both,
+ Size = Vector2.One,
+ Alpha = 0,
+ }
+ };
+ }
+
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
+ {
+ base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
+
+ if (effectPoint.KiaiMode != kiaiDisplayed)
+ {
+ kiaiDisplayed = effectPoint.KiaiMode;
+
+ kiai.ClearTransforms();
+ kiai.FadeTo(kiaiDisplayed ? 1 : 0, 200);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
index 6b59718173..5dfc7ec0df 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.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 System.Collections.Generic;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
@@ -52,7 +53,36 @@ namespace osu.Game.Rulesets.Taiko.Skinning
case TaikoSkinComponents.HitTarget:
if (GetTexture("taikobigcircle") != null)
- return new LegacyHitTarget();
+ return new TaikoLegacyHitTarget();
+
+ return null;
+
+ case TaikoSkinComponents.PlayfieldBackgroundRight:
+ if (GetTexture("taiko-bar-right") != null)
+ return new TaikoLegacyPlayfieldBackgroundRight();
+
+ return null;
+
+ case TaikoSkinComponents.PlayfieldBackgroundLeft:
+ // This is displayed inside LegacyInputDrum. It is required to be there for layout purposes (can be seen on legacy skins).
+ if (GetTexture("taiko-bar-right") != null)
+ return Drawable.Empty();
+
+ return null;
+
+ case TaikoSkinComponents.BarLine:
+ if (GetTexture("taiko-barline") != null)
+ return new LegacyBarLine();
+
+ return null;
+
+ case TaikoSkinComponents.TaikoExplosionGood:
+ case TaikoSkinComponents.TaikoExplosionGreat:
+ case TaikoSkinComponents.TaikoExplosionMiss:
+
+ var sprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
+ if (sprite != null)
+ return new LegacyHitExplosion(sprite);
return null;
}
@@ -60,6 +90,23 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return source.GetDrawableComponent(component);
}
+ private string getHitName(TaikoSkinComponents component)
+ {
+ switch (component)
+ {
+ case TaikoSkinComponents.TaikoExplosionMiss:
+ return "taiko-hit0";
+
+ case TaikoSkinComponents.TaikoExplosionGood:
+ return "taiko-hit100";
+
+ case TaikoSkinComponents.TaikoExplosionGreat:
+ return "taiko-hit300";
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(component), "Invalid result type");
+ }
+
public Texture GetTexture(string componentName) => source.GetTexture(componentName);
public SampleChannel GetSample(ISampleInfo sampleInfo) => source.GetSample(new LegacyTaikoSampleInfo(sampleInfo));
diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
index 775eeb4e38..fd091f97d0 100644
--- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
@@ -11,6 +11,12 @@ namespace osu.Game.Rulesets.Taiko
DrumRollBody,
DrumRollTick,
Swell,
- HitTarget
+ HitTarget,
+ PlayfieldBackgroundLeft,
+ PlayfieldBackgroundRight,
+ BarLine,
+ TaikoExplosionMiss,
+ TaikoExplosionGood,
+ TaikoExplosionGreat,
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
new file mode 100644
index 0000000000..9943a58e3e
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
@@ -0,0 +1,59 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.UI
+{
+ internal class DefaultHitExplosion : CircularContainer
+ {
+ [Resolved]
+ private DrawableHitObject judgedObject { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ BorderColour = Color4.White;
+ BorderThickness = 1;
+
+ Blending = BlendingParameters.Additive;
+
+ Alpha = 0.15f;
+ Masking = true;
+
+ if (judgedObject.Result.Type == HitResult.Miss)
+ return;
+
+ bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim;
+
+ InternalChildren = new[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = isRim ? colours.BlueDarker : colours.PinkDarker,
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ this.ScaleTo(3f, 1000, Easing.OutQuint);
+ this.FadeOut(500);
+
+ Expire(true);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index e4a4b555a7..a6a00fe242 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -49,10 +49,7 @@ namespace osu.Game.Rulesets.Taiko.UI
switch (h)
{
case Hit hit:
- if (hit.Type == HitType.Centre)
- return new DrawableCentreHit(hit);
- else
- return new DrawableRimHit(hit);
+ return new DrawableHit(hit);
case DrumRoll drumRoll:
return new DrawableDrumRoll(drumRoll);
diff --git a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
new file mode 100644
index 0000000000..fde42bec04
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.Taiko.UI
+{
+ internal class DrumRollHitContainer : ScrollingHitObjectContainer
+ {
+ protected override void Update()
+ {
+ base.Update();
+
+ // Remove any auxiliary hit notes that were spawned during a drum roll but subsequently rewound.
+ for (var i = AliveInternalChildren.Count - 1; i >= 0; i--)
+ {
+ var flyingHit = (DrawableFlyingHit)AliveInternalChildren[i];
+ if (Time.Current <= flyingHit.HitObject.StartTime)
+ Remove(flyingHit);
+ }
+ }
+
+ protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
+ {
+ base.OnChildLifetimeBoundaryCrossed(e);
+
+ // ensure all old hits are removed on becoming alive (may miss being in the AliveInternalChildren list above).
+ if (e.Kind == LifetimeBoundaryKind.Start && e.Direction == LifetimeBoundaryCrossingDirection.Backward)
+ Remove((DrawableHitObject)e.Child);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
index 404960c26f..f0585b9c50 100644
--- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
@@ -1,15 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osuTK;
-using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.UI
{
@@ -20,55 +20,49 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public override bool RemoveWhenNotAlive => true;
+ [Cached(typeof(DrawableHitObject))]
public readonly DrawableHitObject JudgedObject;
- private readonly Box innerFill;
+ private SkinnableDrawable skinnable;
- private readonly bool isRim;
+ public override double LifetimeStart => skinnable.Drawable.LifetimeStart;
- public HitExplosion(DrawableHitObject judgedObject, bool isRim)
+ public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd;
+
+ public HitExplosion(DrawableHitObject judgedObject)
{
- this.isRim = isRim;
-
JudgedObject = judgedObject;
- Anchor = Anchor.CentreLeft;
+ Anchor = Anchor.Centre;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
Size = new Vector2(TaikoHitObject.DEFAULT_SIZE);
RelativePositionAxes = Axes.Both;
-
- BorderColour = Color4.White;
- BorderThickness = 1;
-
- Alpha = 0.15f;
- Masking = true;
-
- Children = new[]
- {
- innerFill = new Box
- {
- RelativeSizeAxes = Axes.Both,
- }
- };
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load()
{
- innerFill.Colour = isRim ? colours.BlueDarker : colours.PinkDarker;
+ Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject.Result?.Type ?? HitResult.Great)), _ => new DefaultHitExplosion());
}
- protected override void LoadComplete()
+ private TaikoSkinComponents getComponentName(HitResult resultType)
{
- base.LoadComplete();
+ switch (resultType)
+ {
+ case HitResult.Miss:
+ return TaikoSkinComponents.TaikoExplosionMiss;
- this.ScaleTo(3f, 1000, Easing.OutQuint);
- this.FadeOut(500);
+ case HitResult.Good:
+ return TaikoSkinComponents.TaikoExplosionGood;
- Expire(true);
+ case HitResult.Great:
+ return TaikoSkinComponents.TaikoExplosionGreat;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(resultType), "Invalid result type");
}
///
diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
index 422ea2f929..38026517d9 100644
--- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
@@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Taiko.UI
sampleMapping = new DrumSampleMapping(controlPoints);
RelativeSizeAxes = Axes.Both;
- FillMode = FillMode.Fit;
}
[BackgroundDependencyLoader]
@@ -40,6 +39,8 @@ namespace osu.Game.Rulesets.Taiko.UI
Child = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container
{
RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ Scale = new Vector2(0.9f),
Children = new Drawable[]
{
new TaikoHalfDrum(false)
diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs
index e80b463481..067d390894 100644
--- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs
@@ -18,14 +18,12 @@ namespace osu.Game.Rulesets.Taiko.UI
public override bool RemoveWhenNotAlive => true;
public readonly DrawableHitObject JudgedObject;
+ private readonly HitType type;
- private readonly bool isRim;
-
- public KiaiHitExplosion(DrawableHitObject judgedObject, bool isRim)
+ public KiaiHitExplosion(DrawableHitObject judgedObject, HitType type)
{
- this.isRim = isRim;
-
JudgedObject = judgedObject;
+ this.type = type;
Anchor = Anchor.CentreLeft;
Origin = Anchor.Centre;
@@ -33,6 +31,8 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Both;
Size = new Vector2(TaikoHitObject.DEFAULT_SIZE, 1);
+ Blending = BlendingParameters.Additive;
+
Masking = true;
Alpha = 0.25f;
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.UI
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
- Colour = isRim ? colours.BlueDarker : colours.PinkDarker,
+ Colour = type == HitType.Rim ? colours.BlueDarker : colours.PinkDarker,
Radius = 60,
};
}
diff --git a/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.cs b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.cs
new file mode 100644
index 0000000000..2a8890a95d
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.UI
+{
+ internal class PlayfieldBackgroundLeft : CompositeDrawable
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ RelativeSizeAxes = Axes.Both;
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Gray1,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Box
+ {
+ Anchor = Anchor.TopRight,
+ RelativeSizeAxes = Axes.Y,
+ Width = 10,
+ Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)),
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundRight.cs
new file mode 100644
index 0000000000..44bfdacf37
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundRight.cs
@@ -0,0 +1,61 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.UI
+{
+ public class PlayfieldBackgroundRight : CompositeDrawable
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Name = "Transparent playfield background";
+ RelativeSizeAxes = Axes.Both;
+ Masking = true;
+ BorderColour = colours.Gray1;
+
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Colour = Color4.Black.Opacity(0.2f),
+ Radius = 5,
+ };
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colours.Gray0,
+ Alpha = 0.6f
+ },
+ new Container
+ {
+ Name = "Border",
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ MaskingSmoothness = 0,
+ BorderThickness = 2,
+ AlwaysPresent = true,
+ Children = new[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true
+ }
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/UI/HitTarget.cs b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
similarity index 95%
rename from osu.Game.Rulesets.Taiko/UI/HitTarget.cs
rename to osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
index 88886508af..7de1593ab6 100644
--- a/osu.Game.Rulesets.Taiko/UI/HitTarget.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
@@ -13,14 +13,14 @@ namespace osu.Game.Rulesets.Taiko.UI
///
/// A component that is displayed at the hit position in the taiko playfield.
///
- internal class HitTarget : Container
+ internal class TaikoHitTarget : Container
{
///
/// Thickness of all drawn line pieces.
///
private const float border_thickness = 2.5f;
- public HitTarget()
+ public TaikoHitTarget()
{
RelativeSizeAxes = Axes.Both;
@@ -41,7 +41,6 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
Scale = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE),
Masking = true,
BorderColour = Color4.White,
@@ -63,7 +62,6 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
Scale = new Vector2(TaikoHitObject.DEFAULT_SIZE),
Masking = true,
BorderColour = Color4.White,
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 375d9995c0..5c763cb332 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -3,11 +3,8 @@
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Effects;
-using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
@@ -18,193 +15,138 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Skinning;
-using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.UI
{
public class TaikoPlayfield : ScrollingPlayfield
{
+ private readonly ControlPointInfo controlPoints;
+
///
/// Default height of a when inside a .
///
public const float DEFAULT_HEIGHT = 178;
- ///
- /// The offset from which the center of the hit target lies at.
- ///
- public const float HIT_TARGET_OFFSET = 100;
+ private Container hitExplosionContainer;
+ private Container kiaiExplosionContainer;
+ private JudgementContainer judgementContainer;
+ private ScrollingHitObjectContainer drumRollHitContainer;
+ internal Drawable HitTarget;
- ///
- /// The size of the left area of the playfield. This area contains the input drum.
- ///
- private const float left_area_size = 240;
+ private ProxyContainer topLevelHitContainer;
+ private ProxyContainer barlineContainer;
+ private Container rightArea;
+ private Container leftArea;
- private readonly Container hitExplosionContainer;
- private readonly Container kiaiExplosionContainer;
- private readonly JudgementContainer judgementContainer;
- internal readonly Drawable HitTarget;
-
- private readonly ProxyContainer topLevelHitContainer;
- private readonly ProxyContainer barlineContainer;
-
- private readonly Container overlayBackgroundContainer;
- private readonly Container backgroundContainer;
-
- private readonly Box overlayBackground;
- private readonly Box background;
+ private Container hitTargetOffsetContent;
public TaikoPlayfield(ControlPointInfo controlPoints)
{
- InternalChildren = new Drawable[]
+ this.controlPoints = controlPoints;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ InternalChildren = new[]
{
- backgroundContainer = new Container
- {
- Name = "Transparent playfield background",
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Colour = Color4.Black.Opacity(0.2f),
- Radius = 5,
- },
- Children = new Drawable[]
- {
- background = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0.6f
- },
- }
- },
- new Container
+ new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackgroundRight()),
+ rightArea = new Container
{
Name = "Right area",
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Left = left_area_size },
+ RelativePositionAxes = Axes.Both,
Children = new Drawable[]
{
new Container
{
Name = "Masked elements before hit objects",
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Left = HIT_TARGET_OFFSET },
- Masking = true,
+ FillMode = FillMode.Fit,
Children = new[]
{
hitExplosionContainer = new Container
{
RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
- Blending = BlendingParameters.Additive,
},
- HitTarget = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.HitTarget), _ => new HitTarget())
+ HitTarget = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.HitTarget), _ => new TaikoHitTarget())
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit
}
}
},
- barlineContainer = new ProxyContainer
+ hitTargetOffsetContent = new Container
{
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }
- },
- new Container
- {
- Name = "Hit objects",
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Left = HIT_TARGET_OFFSET },
- Masking = true,
- Child = HitObjectContainer
- },
- kiaiExplosionContainer = new Container
- {
- Name = "Kiai hit explosions",
- RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
- Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
- Blending = BlendingParameters.Additive
- },
- judgementContainer = new JudgementContainer
- {
- Name = "Judgements",
- RelativeSizeAxes = Axes.Y,
- Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
- Blending = BlendingParameters.Additive
+ Children = new Drawable[]
+ {
+ barlineContainer = new ProxyContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Container
+ {
+ Name = "Hit objects",
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ HitObjectContainer,
+ drumRollHitContainer = new DrumRollHitContainer()
+ }
+ },
+ kiaiExplosionContainer = new Container
+ {
+ Name = "Kiai hit explosions",
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ },
+ judgementContainer = new JudgementContainer
+ {
+ Name = "Judgements",
+ RelativeSizeAxes = Axes.Y,
+ },
+ }
},
}
},
- overlayBackgroundContainer = new Container
+ leftArea = new Container
{
Name = "Left overlay",
- RelativeSizeAxes = Axes.Y,
- Size = new Vector2(left_area_size, 1),
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ BorderColour = colours.Gray0,
Children = new Drawable[]
{
- overlayBackground = new Box
- {
- RelativeSizeAxes = Axes.Both,
- },
+ new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new PlayfieldBackgroundLeft()),
new InputDrum(controlPoints)
{
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- Scale = new Vector2(0.9f),
- Margin = new MarginPadding { Right = 20 }
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
},
- new Box
- {
- Anchor = Anchor.TopRight,
- RelativeSizeAxes = Axes.Y,
- Width = 10,
- Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)),
- },
- }
- },
- new Container
- {
- Name = "Border",
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- MaskingSmoothness = 0,
- BorderThickness = 2,
- AlwaysPresent = true,
- Children = new[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- AlwaysPresent = true
- }
}
},
topLevelHitContainer = new ProxyContainer
{
Name = "Top level hit objects",
RelativeSizeAxes = Axes.Both,
- }
+ },
+ drumRollHitContainer.CreateProxy()
};
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ protected override void Update()
{
- overlayBackgroundContainer.BorderColour = colours.Gray0;
- overlayBackground.Colour = colours.Gray1;
+ base.Update();
- backgroundContainer.BorderColour = colours.Gray1;
- background.Colour = colours.Gray0;
+ // Padding is required to be updated for elements which are based on "absolute" X sized elements.
+ // This is basically allowing for correct alignment as relative pieces move around them.
+ rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth };
+ hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
}
public override void Add(DrawableHitObject h)
{
h.OnNewResult += OnNewResult;
-
base.Add(h);
switch (h)
@@ -223,7 +165,6 @@ namespace osu.Game.Rulesets.Taiko.UI
{
if (!DisplayJudgements.Value)
return;
-
if (!judgedObject.DisplayResult)
return;
@@ -234,6 +175,15 @@ namespace osu.Game.Rulesets.Taiko.UI
hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).MainObject)?.VisualiseSecondHit();
break;
+ case TaikoDrumRollTickJudgement _:
+ if (!result.IsHit)
+ break;
+
+ var drawableTick = (DrawableDrumRollTick)judgedObject;
+
+ addDrumRollHit(drawableTick);
+ break;
+
default:
judgementContainer.Add(new DrawableTaikoJudgement(result, judgedObject)
{
@@ -246,17 +196,23 @@ namespace osu.Game.Rulesets.Taiko.UI
if (!result.IsHit)
break;
- bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim;
-
- hitExplosionContainer.Add(new HitExplosion(judgedObject, isRim));
-
- if (judgedObject.HitObject.Kiai)
- kiaiExplosionContainer.Add(new KiaiHitExplosion(judgedObject, isRim));
+ var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre;
+ addExplosion(judgedObject, type);
break;
}
}
+ private void addDrumRollHit(DrawableDrumRollTick drawableTick) =>
+ drumRollHitContainer.Add(new DrawableFlyingHit(drawableTick));
+
+ private void addExplosion(DrawableHitObject drawableObject, HitType type)
+ {
+ hitExplosionContainer.Add(new HitExplosion(drawableObject));
+ if (drawableObject.HitObject.Kiai)
+ kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type));
+ }
+
private class ProxyContainer : LifetimeManagementContainer
{
public new MarginPadding Padding
diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
index c6095ae404..ba6f5fc85c 100644
--- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportWhenClosed()
{
- //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
+ // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWhenClosed)))
{
try
@@ -48,7 +48,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportThenDelete()
{
- //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
+ // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenDelete)))
{
try
@@ -69,7 +69,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportThenImport()
{
- //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
+ // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImport)))
{
try
@@ -96,7 +96,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportCorruptThenImport()
{
- //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
+ // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportCorruptThenImport)))
{
try
@@ -138,7 +138,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestRollbackOnFailure()
{
- //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
+ // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestRollbackOnFailure)))
{
try
@@ -215,7 +215,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportThenImportDifferentHash()
{
- //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
+ // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportDifferentHash)))
{
try
@@ -246,7 +246,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportThenDeleteThenImport()
{
- //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
+ // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenDeleteThenImport)))
{
try
@@ -274,7 +274,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[TestCase(false)]
public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
{
- //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
+ // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(TestImportThenDeleteThenImportWithOnlineIDMismatch)}-{set}"))
{
try
@@ -308,7 +308,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportWithDuplicateBeatmapIDs()
{
- //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
+ // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateBeatmapIDs)))
{
try
@@ -695,12 +695,12 @@ namespace osu.Game.Tests.Beatmaps.IO
waitForOrAssert(() => (resultSets = store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526)).Any(),
@"BeatmapSet did not import to the database in allocated time.", timeout);
- //ensure we were stored to beatmap database backing...
+ // ensure we were stored to beatmap database backing...
Assert.IsTrue(resultSets.Count() == 1, $@"Incorrect result count found ({resultSets.Count()} but should be 1).");
IEnumerable queryBeatmaps() => store.QueryBeatmaps(s => s.BeatmapSet.OnlineBeatmapSetID == 241526 && s.BaseDifficultyID > 0);
IEnumerable queryBeatmapSets() => store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526);
- //if we don't re-check here, the set will be inserted but the beatmaps won't be present yet.
+ // if we don't re-check here, the set will be inserted but the beatmaps won't be present yet.
waitForOrAssert(() => queryBeatmaps().Count() == 12,
@"Beatmaps did not import to the database in allocated time", timeout);
waitForOrAssert(() => queryBeatmapSets().Count() == 1,
diff --git a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs
index 2d4587341d..b7b48ec06a 100644
--- a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs
+++ b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs
@@ -1,9 +1,9 @@
// 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 Microsoft.EntityFrameworkCore.Internal;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets.Objects;
@@ -137,7 +137,7 @@ namespace osu.Game.Tests.Beatmaps
var hitCircle = new HitCircle { StartTime = 1000 };
editorBeatmap.Add(hitCircle);
Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1));
- Assert.That(editorBeatmap.HitObjects.IndexOf(hitCircle), Is.EqualTo(3));
+ Assert.That(Array.IndexOf(editorBeatmap.HitObjects.ToArray(), hitCircle), Is.EqualTo(3));
}
///
@@ -161,7 +161,7 @@ namespace osu.Game.Tests.Beatmaps
hitCircle.StartTime = 0;
Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1));
- Assert.That(editorBeatmap.HitObjects.IndexOf(hitCircle), Is.EqualTo(1));
+ Assert.That(Array.IndexOf(editorBeatmap.HitObjects.ToArray(), hitCircle), Is.EqualTo(1));
}
///
diff --git a/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
similarity index 98%
rename from osu.Game.Tests/Editor/EditorChangeHandlerTest.cs
rename to osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
index 9613f250c4..feda1ae0e9 100644
--- a/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs
+++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
@@ -5,7 +5,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Screens.Edit;
-namespace osu.Game.Tests.Editor
+namespace osu.Game.Tests.Editing
{
[TestFixture]
public class EditorChangeHandlerTest
diff --git a/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs
similarity index 91%
rename from osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs
rename to osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs
index c24418d688..ff17f23d50 100644
--- a/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs
+++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs
@@ -17,7 +17,7 @@ using osu.Game.Screens.Edit;
using osuTK;
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
-namespace osu.Game.Tests.Editor
+namespace osu.Game.Tests.Editing
{
[TestFixture]
public class LegacyEditorBeatmapPatcherTest
@@ -304,6 +304,31 @@ namespace osu.Game.Tests.Editor
runTest(patch);
}
+ [Test]
+ public void TestChangeHitObjectAtSameTime()
+ {
+ current.AddRange(new[]
+ {
+ new HitCircle { StartTime = 500, Position = new Vector2(50) },
+ new HitCircle { StartTime = 500, Position = new Vector2(100) },
+ new HitCircle { StartTime = 500, Position = new Vector2(150) },
+ new HitCircle { StartTime = 500, Position = new Vector2(200) },
+ });
+
+ var patch = new OsuBeatmap
+ {
+ HitObjects =
+ {
+ new HitCircle { StartTime = 500, Position = new Vector2(150) },
+ new HitCircle { StartTime = 500, Position = new Vector2(100) },
+ new HitCircle { StartTime = 500, Position = new Vector2(50) },
+ new HitCircle { StartTime = 500, Position = new Vector2(200) },
+ }
+ };
+
+ runTest(patch);
+ }
+
private void runTest(IBeatmap patch)
{
// Due to the method of testing, "patch" comes in without having been decoded via a beatmap decoder.
diff --git a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
similarity index 99%
rename from osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs
rename to osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
index 3cb5909ba9..168ec0f09d 100644
--- a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -14,7 +14,7 @@ using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
-namespace osu.Game.Tests.Editor
+namespace osu.Game.Tests.Editing
{
[HeadlessTest]
public class TestSceneHitObjectComposerDistanceSnapping : EditorClockTestScene
diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
index 158954106d..830e4bc603 100644
--- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
+++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
- cpi.Add(1000, new TimingControlPoint()); //is redundant
+ cpi.Add(1000, new TimingControlPoint()); // is redundant
Assert.That(cpi.Groups.Count, Is.EqualTo(2));
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
diff --git a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs
index 7df7df22ea..92a60663de 100644
--- a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs
+++ b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs
@@ -46,12 +46,12 @@ namespace osu.Game.Tests.NonVisual
confirmCurrentFrame(0);
confirmNextFrame(1);
- //if we hit the first frame perfectly, time should progress to it.
+ // if we hit the first frame perfectly, time should progress to it.
setTime(1000, 1000);
confirmCurrentFrame(1);
confirmNextFrame(2);
- //in between non-important frames should progress based on input.
+ // in between non-important frames should progress based on input.
setTime(1200, 1200);
confirmCurrentFrame(1);
@@ -144,7 +144,7 @@ namespace osu.Game.Tests.NonVisual
confirmCurrentFrame(2);
confirmNextFrame(1);
- //ensure each frame plays out until start
+ // ensure each frame plays out until start
setTime(-500, 1000);
confirmCurrentFrame(1);
confirmNextFrame(0);
diff --git a/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs b/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs
new file mode 100644
index 0000000000..62c7732b66
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs
@@ -0,0 +1,85 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Utils;
+using osu.Game.Utils;
+
+namespace osu.Game.Tests.NonVisual
+{
+ [TestFixture]
+ public class PeriodTrackerTest
+ {
+ private static readonly Period[] single_period = { new Period(1.0, 2.0) };
+
+ private static readonly Period[] unordered_periods =
+ {
+ new Period(-9.1, -8.3),
+ new Period(-3.4, 2.1),
+ new Period(9.0, 50.0),
+ new Period(5.25, 10.50)
+ };
+
+ [Test]
+ public void TestCheckValueInsideSinglePeriod()
+ {
+ var tracker = new PeriodTracker(single_period);
+
+ var period = single_period.Single();
+ Assert.IsTrue(tracker.IsInAny(period.Start));
+ Assert.IsTrue(tracker.IsInAny(getMidpoint(period)));
+ Assert.IsTrue(tracker.IsInAny(period.End));
+ }
+
+ [Test]
+ public void TestCheckValuesInsidePeriods()
+ {
+ var tracker = new PeriodTracker(unordered_periods);
+
+ foreach (var period in unordered_periods)
+ Assert.IsTrue(tracker.IsInAny(getMidpoint(period)));
+ }
+
+ [Test]
+ public void TestCheckValuesInRandomOrder()
+ {
+ var tracker = new PeriodTracker(unordered_periods);
+
+ foreach (var period in unordered_periods.OrderBy(_ => RNG.Next()))
+ Assert.IsTrue(tracker.IsInAny(getMidpoint(period)));
+ }
+
+ [Test]
+ public void TestCheckValuesOutOfPeriods()
+ {
+ var tracker = new PeriodTracker(new[]
+ {
+ new Period(1.0, 2.0),
+ new Period(3.0, 4.0)
+ });
+
+ Assert.IsFalse(tracker.IsInAny(0.9), "Time before first period is being considered inside");
+
+ Assert.IsFalse(tracker.IsInAny(2.1), "Time right after first period is being considered inside");
+ Assert.IsFalse(tracker.IsInAny(2.9), "Time right before second period is being considered inside");
+
+ Assert.IsFalse(tracker.IsInAny(4.1), "Time after last period is being considered inside");
+ }
+
+ [Test]
+ public void TestReversedPeriodHandling()
+ {
+ Assert.Throws(() =>
+ {
+ _ = new PeriodTracker(new[]
+ {
+ new Period(2.0, 1.0)
+ });
+ });
+ }
+
+ private double getMidpoint(Period period) => period.Start + (period.End - period.Start) / 2;
+ }
+}
diff --git a/osu.Game.Tests/Resources/sample-beatmap-catch.osu b/osu.Game.Tests/Resources/sample-beatmap-catch.osu
new file mode 100644
index 0000000000..09ef762e3e
--- /dev/null
+++ b/osu.Game.Tests/Resources/sample-beatmap-catch.osu
@@ -0,0 +1,30 @@
+osu file format v14
+
+[General]
+SampleSet: Normal
+StackLeniency: 0.7
+Mode: 2
+
+[Difficulty]
+HPDrainRate:3
+CircleSize:5
+OverallDifficulty:8
+ApproachRate:8
+SliderMultiplier:3.59999990463257
+SliderTickRate:2
+
+[TimingPoints]
+24,352.941176470588,4,1,1,100,1,0
+6376,-50,4,1,1,100,0,0
+
+[HitObjects]
+32,183,24,5,0,0:0:0:0:
+106,123,200,1,10,0:0:0:0:
+199,108,376,1,2,0:0:0:0:
+305,105,553,5,4,0:0:0:0:
+386,112,729,1,14,0:0:0:0:
+486,197,906,5,12,0:0:0:0:
+14,199,1082,2,0,L|473:198,1,449.999988079071
+14,199,1700,6,6,P|248:33|490:222,1,629.9999833107,0|8,0:0|0:0,0:0:0:0:
+10,190,2494,2,8,B|252:29|254:335|468:167,1,449.999988079071,10|12,0:0|0:0,0:0:0:0:
+256,192,3112,12,0,3906,0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/sample-beatmap-mania.osu b/osu.Game.Tests/Resources/sample-beatmap-mania.osu
new file mode 100644
index 0000000000..04d6a31ab6
--- /dev/null
+++ b/osu.Game.Tests/Resources/sample-beatmap-mania.osu
@@ -0,0 +1,39 @@
+osu file format v14
+
+[General]
+SampleSet: Normal
+StackLeniency: 0.7
+Mode: 3
+
+[Difficulty]
+HPDrainRate:3
+CircleSize:5
+OverallDifficulty:8
+ApproachRate:8
+SliderMultiplier:3.59999990463257
+SliderTickRate:2
+
+[TimingPoints]
+24,352.941176470588,4,1,1,100,1,0
+6376,-50,4,1,1,100,0,0
+
+[HitObjects]
+51,192,24,1,0,0:0:0:0:
+153,192,200,1,0,0:0:0:0:
+358,192,376,1,0,0:0:0:0:
+460,192,553,1,0,0:0:0:0:
+460,192,729,128,0,1435:0:0:0:0:
+358,192,906,128,0,1612:0:0:0:0:
+256,192,1082,128,0,1788:0:0:0:0:
+153,192,1259,128,0,1965:0:0:0:0:
+51,192,1435,128,0,2141:0:0:0:0:
+51,192,2318,1,12,0:0:0:0:
+153,192,2318,1,4,0:0:0:0:
+256,192,2318,1,6,0:0:0:0:
+358,192,2318,1,14,0:0:0:0:
+460,192,2318,1,0,0:0:0:0:
+51,192,2494,128,0,2582:0:0:0:0:
+153,192,2494,128,14,2582:0:0:0:0:
+256,192,2494,128,6,2582:0:0:0:0:
+358,192,2494,128,4,2582:0:0:0:0:
+460,192,2494,128,12,2582:0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/sample-beatmap-taiko.osu b/osu.Game.Tests/Resources/sample-beatmap-taiko.osu
new file mode 100644
index 0000000000..94b4288336
--- /dev/null
+++ b/osu.Game.Tests/Resources/sample-beatmap-taiko.osu
@@ -0,0 +1,42 @@
+osu file format v14
+
+[General]
+SampleSet: Normal
+StackLeniency: 0.7
+Mode: 1
+
+[Difficulty]
+HPDrainRate:3
+CircleSize:5
+OverallDifficulty:8
+ApproachRate:8
+SliderMultiplier:3.59999990463257
+SliderTickRate:2
+
+[TimingPoints]
+24,352.941176470588,4,1,1,100,1,0
+6376,-50,4,1,1,100,0,0
+
+[HitObjects]
+231,129,24,1,0,0:0:0:0:
+231,129,200,1,0,0:0:0:0:
+231,129,376,1,0,0:0:0:0:
+231,129,553,1,0,0:0:0:0:
+231,129,729,1,0,0:0:0:0:
+373,132,906,1,4,0:0:0:0:
+373,132,1082,1,4,0:0:0:0:
+373,132,1259,1,4,0:0:0:0:
+373,132,1435,1,4,0:0:0:0:
+231,129,1788,1,8,0:0:0:0:
+231,129,1964,1,8,0:0:0:0:
+231,129,2140,1,8,0:0:0:0:
+231,129,2317,1,8,0:0:0:0:
+231,129,2493,1,8,0:0:0:0:
+373,132,2670,1,12,0:0:0:0:
+373,132,2846,1,12,0:0:0:0:
+373,132,3023,1,12,0:0:0:0:
+373,132,3199,1,12,0:0:0:0:
+51,189,3553,2,0,L|150:188,1,89.9999976158143
+52,191,3906,2,0,L|512:189,1,449.999988079071
+26,196,4612,2,4,L|501:195,1,449.999988079071
+17,242,5318,2,10,P|250:69|495:243,1,629.9999833107,0|8,0:0|0:0,0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
similarity index 98%
rename from osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
index fd7a5980f3..f6e69fd8bf 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
@@ -14,7 +14,7 @@ using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene
{
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
similarity index 96%
rename from osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
index a8830824c0..6f5655006e 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneComposeScreen : EditorClockTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
similarity index 99%
rename from osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
index f49256a633..417d16fdb0 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
@@ -13,7 +13,7 @@ using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneDistanceSnapGrid : EditorClockTestScene
{
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs
similarity index 84%
rename from osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs
index efc2a6f552..20862e9cac 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs
@@ -4,33 +4,27 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
-using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
-using osu.Game.Screens.Edit.Compose.Components.Timeline;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
- public class TestSceneEditorChangeStates : ScreenTestScene
+ public class TestSceneEditorChangeStates : EditorTestScene
{
+ public TestSceneEditorChangeStates()
+ : base(new OsuRuleset())
+ {
+ }
+
private EditorBeatmap editorBeatmap;
- private TestEditor editor;
public override void SetUpSteps()
{
base.SetUpSteps();
- AddStep("load editor", () =>
- {
- Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
- LoadScreen(editor = new TestEditor());
- });
-
- AddUntilStep("wait for editor to load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true
- && editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
- AddStep("get beatmap", () => editorBeatmap = editor.ChildrenOfType().Single());
+ AddStep("get beatmap", () => editorBeatmap = Editor.ChildrenOfType().Single());
}
[Test]
@@ -158,11 +152,13 @@ namespace osu.Game.Tests.Visual.Editor
AddAssert("no hitobject added", () => addedObject == null);
}
- private void addUndoSteps() => AddStep("undo", () => editor.Undo());
+ private void addUndoSteps() => AddStep("undo", () => ((TestEditor)Editor).Undo());
- private void addRedoSteps() => AddStep("redo", () => editor.Redo());
+ private void addRedoSteps() => AddStep("redo", () => ((TestEditor)Editor).Redo());
- private class TestEditor : Screens.Edit.Editor
+ protected override Editor CreateEditor() => new TestEditor();
+
+ private class TestEditor : Editor
{
public new void Undo() => base.Undo();
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeRadioButtons.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs
similarity index 97%
rename from osu.Game.Tests/Visual/Editor/TestSceneEditorComposeRadioButtons.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs
index 1709067d5d..2deeaef1f6 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeRadioButtons.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs
@@ -7,7 +7,7 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Edit.Components.RadioButtons;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneEditorComposeRadioButtons : OsuTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorMenuBar.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs
similarity index 99%
rename from osu.Game.Tests/Visual/Editor/TestSceneEditorMenuBar.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs
index 53c2d62067..2cbdacb61c 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorMenuBar.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs
@@ -10,7 +10,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit.Components.Menus;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneEditorMenuBar : OsuTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
similarity index 99%
rename from osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
index 3118e0cabe..41d1459103 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
@@ -13,7 +13,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneEditorSeekSnapping : EditorClockTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
similarity index 95%
rename from osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
index 2e04eb50ca..c92423545d 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
@@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osuTK;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneEditorSummaryTimeline : EditorClockTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
similarity index 98%
rename from osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
index e41c2427fb..ddaca26220 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
@@ -20,7 +20,7 @@ using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneHitObjectComposer : EditorClockTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
similarity index 96%
rename from osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs
rename to osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
index 0d4fe4366d..3af976cae0 100644
--- a/osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
@@ -9,7 +9,7 @@ using osu.Game.Beatmaps;
using osu.Game.Screens.Edit.Components;
using osuTK;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestScenePlaybackControl : OsuTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs
similarity index 95%
rename from osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs
index 4d8f877575..5ab2f49b4a 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs
@@ -7,7 +7,7 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneTimelineBlueprintContainer : TimelineTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimelineTickDisplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs
similarity index 95%
rename from osu.Game.Tests/Visual/Editor/TestSceneTimelineTickDisplay.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs
index 43a3cd6122..e33040acdc 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneTimelineTickDisplay.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs
@@ -8,7 +8,7 @@ using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneTimelineTickDisplay : TimelineTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
similarity index 96%
rename from osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
index ae09a7fa47..a6dbe9571e 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneTimingScreen : EditorClockTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs b/osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs
similarity index 98%
rename from osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs
index e2762f3d5f..0c1296b82c 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs
@@ -14,7 +14,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu;
using osuTK.Graphics;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneWaveform : OsuTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
similarity index 99%
rename from osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
index 19d19c2759..082268d824 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
@@ -15,7 +15,7 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneZoomableScrollContainer : OsuManualInputManagerTestScene
{
diff --git a/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
similarity index 99%
rename from osu.Game.Tests/Visual/Editor/TimelineTestScene.cs
rename to osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
index 7081eb3af5..56b2860e96 100644
--- a/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs
+++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
@@ -18,7 +18,7 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
public abstract class TimelineTestScene : EditorClockTestScene
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
index 4b1c2ec256..0be949650e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep($"seek to break {breakIndex}", () => Player.GameplayClockContainer.Seek(destBreak().StartTime));
AddUntilStep("wait for seek to complete", () => Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= destBreak().StartTime);
- BreakPeriod destBreak() => Player.ChildrenOfType().First().Breaks.ElementAt(breakIndex);
+ BreakPeriod destBreak() => Beatmap.Value.Beatmap.Breaks.ElementAt(breakIndex);
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs
index 91d6f2f143..a6f996c30d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs
@@ -97,8 +97,6 @@ namespace osu.Game.Tests.Visual.Gameplay
loadBreaksStep("multiple breaks", testBreaks);
seekAndAssertBreak("seek to break start", testBreaks[1].StartTime, true);
- AddAssert("is skipped to break #2", () => breakTracker.CurrentBreakIndex == 1);
-
seekAndAssertBreak("seek to break middle", testBreaks[1].StartTime + testBreaks[1].Duration / 2, true);
seekAndAssertBreak("seek to break end", testBreaks[1].EndTime, false);
seekAndAssertBreak("seek to break after end", testBreaks[1].EndTime + 500, false);
@@ -174,8 +172,6 @@ namespace osu.Game.Tests.Visual.Gameplay
private readonly ManualClock manualClock;
private IFrameBasedClock originalClock;
- public new int CurrentBreakIndex => base.CurrentBreakIndex;
-
public double ManualClockTime
{
get => manualClock.CurrentTime;
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
index f24589ed35..8fbbc8ebd8 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
@@ -5,13 +5,17 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
using osu.Game.Overlays.Toolbar;
+using osu.Game.Rulesets;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Menus
{
[TestFixture]
- public class TestSceneToolbar : OsuTestScene
+ public class TestSceneToolbar : OsuManualInputManagerTestScene
{
public override IReadOnlyList RequiredTypes => new[]
{
@@ -21,24 +25,62 @@ namespace osu.Game.Tests.Visual.Menus
typeof(ToolbarNotificationButton),
};
- public TestSceneToolbar()
+ private Toolbar toolbar;
+
+ [Resolved]
+ private RulesetStore rulesets { get; set; }
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ Child = toolbar = new Toolbar { State = { Value = Visibility.Visible } };
+ });
+
+ [Test]
+ public void TestNotificationCounter()
{
- var toolbar = new Toolbar { State = { Value = Visibility.Visible } };
ToolbarNotificationButton notificationButton = null;
- AddStep("create toolbar", () =>
- {
- Add(toolbar);
- notificationButton = toolbar.Children.OfType().Last().Children.OfType().First();
- });
-
- void setNotifications(int count) => AddStep($"set notification count to {count}", () => notificationButton.NotificationCount.Value = count);
+ AddStep("retrieve notification button", () => notificationButton = toolbar.ChildrenOfType().Single());
setNotifications(1);
setNotifications(2);
setNotifications(3);
setNotifications(0);
setNotifications(144);
+
+ void setNotifications(int count)
+ => AddStep($"set notification count to {count}",
+ () => notificationButton.NotificationCount.Value = count);
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestRulesetSwitchingShortcut(bool toolbarHidden)
+ {
+ ToolbarRulesetSelector rulesetSelector = null;
+
+ if (toolbarHidden)
+ AddStep("hide toolbar", () => toolbar.Hide());
+
+ AddStep("retrieve ruleset selector", () => rulesetSelector = toolbar.ChildrenOfType().Single());
+
+ for (int i = 0; i < 4; i++)
+ {
+ var expected = rulesets.AvailableRulesets.ElementAt(i);
+ var numberKey = Key.Number1 + i;
+
+ AddStep($"switch to ruleset {i} via shortcut", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.PressKey(numberKey);
+
+ InputManager.ReleaseKey(Key.ControlLeft);
+ InputManager.ReleaseKey(numberKey);
+ });
+
+ AddUntilStep("ruleset switched", () => rulesetSelector.Current.Value.Equals(expected));
+ }
}
}
}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
index 52d8ea0480..328a0e0c27 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
@@ -20,6 +20,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
+using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Expanded;
using osu.Game.Screens.Ranking.Expanded.Accuracy;
using osu.Game.Screens.Ranking.Expanded.Statistics;
@@ -74,6 +75,8 @@ namespace osu.Game.Tests.Visual.Ranking
{
var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0));
beatmap.Metadata.Author = author;
+ beatmap.Metadata.Title = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap title";
+ beatmap.Metadata.Artist = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap artist";
return new TestWorkingBeatmap(beatmap);
}
@@ -114,7 +117,7 @@ namespace osu.Game.Tests.Visual.Ranking
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
- Size = new Vector2(500, 700);
+ Size = new Vector2(ScorePanel.EXPANDED_WIDTH, 700);
Children = new Drawable[]
{
new Box
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 39e04ed39a..81fd1b66e5 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -24,10 +24,12 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko;
+using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Filter;
+using osu.Game.Users;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
@@ -110,7 +112,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
- AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);
+ waitForInitialSelection();
WorkingBeatmap selected = null;
@@ -135,7 +137,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
- AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);
+ waitForInitialSelection();
WorkingBeatmap selected = null;
@@ -189,7 +191,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
- AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);
+ waitForInitialSelection();
WorkingBeatmap selected = null;
@@ -769,6 +771,76 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo == groupIcon.Items.First().Beatmap);
}
+ [Test]
+ public void TestChangeRulesetWhilePresentingScore()
+ {
+ BeatmapInfo getPresentBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 0);
+ BeatmapInfo getSwitchBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 1);
+
+ changeRuleset(0);
+
+ createSongSelect();
+
+ addRulesetImportStep(0);
+ addRulesetImportStep(1);
+
+ AddStep("present score", () =>
+ {
+ // this ruleset change should be overridden by the present.
+ Ruleset.Value = getSwitchBeatmap().Ruleset;
+
+ songSelect.PresentScore(new ScoreInfo
+ {
+ User = new User { Username = "woo" },
+ Beatmap = getPresentBeatmap(),
+ Ruleset = getPresentBeatmap().Ruleset
+ });
+ });
+
+ AddUntilStep("wait for results screen presented", () => !songSelect.IsCurrentScreen());
+
+ AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.Equals(getPresentBeatmap()));
+ AddAssert("check ruleset is correct for score", () => Ruleset.Value.ID == 0);
+ }
+
+ [Test]
+ public void TestChangeBeatmapWhilePresentingScore()
+ {
+ BeatmapInfo getPresentBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 0);
+ BeatmapInfo getSwitchBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 1);
+
+ changeRuleset(0);
+
+ addRulesetImportStep(0);
+ addRulesetImportStep(1);
+
+ createSongSelect();
+
+ AddStep("present score", () =>
+ {
+ // this beatmap change should be overridden by the present.
+ Beatmap.Value = manager.GetWorkingBeatmap(getSwitchBeatmap());
+
+ songSelect.PresentScore(new ScoreInfo
+ {
+ User = new User { Username = "woo" },
+ Beatmap = getPresentBeatmap(),
+ Ruleset = getPresentBeatmap().Ruleset
+ });
+ });
+
+ AddUntilStep("wait for results screen presented", () => !songSelect.IsCurrentScreen());
+
+ AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.Equals(getPresentBeatmap()));
+ AddAssert("check ruleset is correct for score", () => Ruleset.Value.ID == 0);
+ }
+
+ private void waitForInitialSelection()
+ {
+ AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);
+ AddUntilStep("wait for difficulty panels visible", () => songSelect.Carousel.ChildrenOfType().Any());
+ }
+
private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info);
private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap);
@@ -797,6 +869,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
AddStep("create song select", () => LoadScreen(songSelect = new TestSongSelect()));
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen());
+ AddUntilStep("wait for carousel loaded", () => songSelect.Carousel.IsAlive);
}
private void addManyTestMaps()
@@ -875,6 +948,8 @@ namespace osu.Game.Tests.Visual.SongSelect
public WorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap;
public new BeatmapCarousel Carousel => base.Carousel;
+ public new void PresentScore(ScoreInfo score) => base.PresentScore(score);
+
protected override bool OnStart()
{
StartRequested?.Invoke();
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 2294cd6966..ec6ee6bc83 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -14,8 +14,6 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Mods.Sections;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Mania;
-using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
@@ -117,8 +115,6 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestManiaMods()
{
changeRuleset(3);
-
- testRankedText(new ManiaRuleset().GetModsFor(ModType.Conversion).First(m => m is ManiaModRandom));
}
[Test]
@@ -217,15 +213,6 @@ namespace osu.Game.Tests.Visual.UserInterface
checkLabelColor(() => Color4.White);
}
- private void testRankedText(Mod mod)
- {
- AddUntilStep("check for ranked", () => modSelect.UnrankedLabel.Alpha == 0);
- selectNext(mod);
- AddUntilStep("check for unranked", () => modSelect.UnrankedLabel.Alpha != 0);
- selectPrevious(mod);
- AddUntilStep("check for ranked", () => modSelect.UnrankedLabel.Alpha == 0);
- }
-
private void selectNext(Mod mod) => AddStep($"left click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(1));
private void selectPrevious(Mod mod) => AddStep($"right click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(-1));
@@ -272,7 +259,6 @@ namespace osu.Game.Tests.Visual.UserInterface
}
public new OsuSpriteText MultiplierLabel => base.MultiplierLabel;
- public new OsuSpriteText UnrankedLabel => base.UnrankedLabel;
public new TriangleButton DeselectAllButton => base.DeselectAllButton;
public new Color4 LowMultiplierColour => base.LowMultiplierColour;
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs
index 2ea9aec50a..532744a0fc 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs
@@ -1,12 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Graphics;
-using osu.Framework.Utils;
+using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
+using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
namespace osu.Game.Tests.Visual.UserInterface
@@ -21,9 +24,14 @@ namespace osu.Game.Tests.Visual.UserInterface
private NowPlayingOverlay nowPlayingOverlay;
+ private RulesetStore rulesets;
+
[BackgroundDependencyLoader]
- private void load()
+ private void load(AudioManager audio, GameHost host)
{
+ Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
+ Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
+
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
nowPlayingOverlay = new NowPlayingOverlay
@@ -44,21 +52,43 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep(@"hide", () => nowPlayingOverlay.Hide());
}
+ private BeatmapManager manager { get; set; }
+
+ private int importId;
+
[Test]
public void TestPrevTrackBehavior()
{
- AddStep(@"Play track", () =>
+ // ensure we have at least two beatmaps available.
+ AddRepeatStep("import beatmap", () => manager.Import(new BeatmapSetInfo
{
- musicController.NextTrack();
- currentBeatmap = Beatmap.Value;
- });
+ Beatmaps = new List
+ {
+ new BeatmapInfo
+ {
+ BaseDifficulty = new BeatmapDifficulty(),
+ }
+ },
+ Metadata = new BeatmapMetadata
+ {
+ Artist = $"a test map {importId++}",
+ Title = "title",
+ }
+ }).Wait(), 5);
+
+ AddStep(@"Next track", () => musicController.NextTrack());
+ AddStep("Store track", () => currentBeatmap = Beatmap.Value);
AddStep(@"Seek track to 6 second", () => musicController.SeekTo(6000));
AddUntilStep(@"Wait for current time to update", () => currentBeatmap.Track.CurrentTime > 5000);
- AddAssert(@"Check action is restart track", () => musicController.PreviousTrack() == PreviousTrackResult.Restart);
- AddUntilStep("Wait for current time to update", () => Precision.AlmostEquals(currentBeatmap.Track.CurrentTime, 0));
- AddAssert(@"Check track didn't change", () => currentBeatmap == Beatmap.Value);
- AddAssert(@"Check action is not restart", () => musicController.PreviousTrack() != PreviousTrackResult.Restart);
+
+ AddStep(@"Set previous", () => musicController.PreviousTrack());
+
+ AddAssert(@"Check beatmap didn't change", () => currentBeatmap == Beatmap.Value);
+ AddUntilStep("Wait for current time to update", () => currentBeatmap.Track.CurrentTime < 5000);
+
+ AddStep(@"Set previous", () => musicController.PreviousTrack());
+ AddAssert(@"Check beatmap did change", () => currentBeatmap != Beatmap.Value);
}
}
}
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 35eb3fa161..5ee887cb64 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -3,7 +3,7 @@
-
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 3b45fc83fd..aa37326a49 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs
index 8d766ec9ba..e86fd890c1 100644
--- a/osu.Game.Tournament/Components/SongBar.cs
+++ b/osu.Game.Tournament/Components/SongBar.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Tournament.Components
{
private BeatmapInfo beatmap;
- private const float height = 145;
+ public const float HEIGHT = 145 / 2f;
[Resolved]
private IBindable ruleset { get; set; }
@@ -157,7 +157,7 @@ namespace osu.Game.Tournament.Components
new Container
{
RelativeSizeAxes = Axes.X,
- Height = height / 2,
+ Height = HEIGHT,
Width = 0.5f,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
@@ -229,7 +229,7 @@ namespace osu.Game.Tournament.Components
{
RelativeSizeAxes = Axes.X,
Width = 0.5f,
- Height = height / 2,
+ Height = HEIGHT,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
}
diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs
index eefa9fcfe6..53ba597a7e 100644
--- a/osu.Game.Tournament/IPC/FileBasedIPC.cs
+++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs
@@ -8,7 +8,6 @@ using Microsoft.Win32;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Framework.Platform;
-using osu.Framework.Platform.Windows;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
@@ -52,7 +51,12 @@ namespace osu.Game.Tournament.IPC
try
{
- Storage = new StableStorage(host as DesktopGameHost);
+ var path = findStablePath();
+
+ if (string.IsNullOrEmpty(path))
+ return null;
+
+ Storage = new DesktopStorage(path, host as DesktopGameHost);
const string file_ipc_filename = "ipc.txt";
const string file_ipc_state_filename = "ipc-state.txt";
@@ -145,64 +149,50 @@ namespace osu.Game.Tournament.IPC
return Storage;
}
- ///
- /// A method of accessing an osu-stable install in a controlled fashion.
- ///
- private class StableStorage : WindowsStorage
+ private string findStablePath()
{
- protected override string LocateBasePath()
- {
- static bool checkExists(string p)
- {
- return File.Exists(Path.Combine(p, "ipc.txt"));
- }
+ static bool checkExists(string p) => File.Exists(Path.Combine(p, "ipc.txt"));
- string stableInstallPath = string.Empty;
+ string stableInstallPath = string.Empty;
+
+ try
+ {
+ try
+ {
+ stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH");
+
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+ }
+ catch
+ {
+ }
try
{
- try
- {
- stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH");
+ using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
+ stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
- if (checkExists(stableInstallPath))
- return stableInstallPath;
- }
- catch
- {
- }
-
- try
- {
- using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
- stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
-
- if (checkExists(stableInstallPath))
- return stableInstallPath;
- }
- catch
- {
- }
-
- stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
if (checkExists(stableInstallPath))
return stableInstallPath;
-
- stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
- if (checkExists(stableInstallPath))
- return stableInstallPath;
-
- return null;
}
- finally
+ catch
{
- Logger.Log($"Stable path for tourney usage: {stableInstallPath}");
}
- }
- public StableStorage(DesktopGameHost host)
- : base(string.Empty, host)
+ stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ return null;
+ }
+ finally
{
+ Logger.Log($"Stable path for tourney usage: {stableInstallPath}");
}
}
}
diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs
index c2e6da9ca5..7794019437 100644
--- a/osu.Game.Tournament/Models/LadderInfo.cs
+++ b/osu.Game.Tournament/Models/LadderInfo.cs
@@ -32,5 +32,11 @@ namespace osu.Game.Tournament.Models
MinValue = 640,
MaxValue = 1366,
};
+
+ public Bindable PlayersPerTeam = new BindableInt(4)
+ {
+ MinValue = 3,
+ MaxValue = 4,
+ };
}
}
diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
index 64a5cd6dec..e4e3842369 100644
--- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Tournament.Screens.Gameplay
[Resolved]
private TournamentMatchChatDisplay chat { get; set; }
- private Box chroma;
+ private Drawable chroma;
[BackgroundDependencyLoader]
private void load(LadderInfo ladder, MatchIPCInfo ipc, Storage storage)
@@ -61,16 +61,30 @@ namespace osu.Game.Tournament.Screens.Gameplay
Y = 110,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
- Children = new Drawable[]
+ Children = new[]
{
- chroma = new Box
+ chroma = new Container
{
- // chroma key area for stable gameplay
- Name = "chroma",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Height = 512,
- Colour = new Color4(0, 255, 0, 255),
+ Children = new Drawable[]
+ {
+ new ChromaArea
+ {
+ Name = "Left chroma",
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.5f,
+ },
+ new ChromaArea
+ {
+ Name = "Right chroma",
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ Width = 0.5f,
+ }
+ }
},
}
},
@@ -98,9 +112,15 @@ namespace osu.Game.Tournament.Screens.Gameplay
},
new SettingsSlider
{
- LabelText = "Chroma Width",
+ LabelText = "Chroma width",
Bindable = LadderInfo.ChromaKeyWidth,
KeyboardStep = 1,
+ },
+ new SettingsSlider
+ {
+ LabelText = "Players per team",
+ Bindable = LadderInfo.PlayersPerTeam,
+ KeyboardStep = 1,
}
}
}
@@ -201,5 +221,54 @@ namespace osu.Game.Tournament.Screens.Gameplay
lastState = state.NewValue;
}
}
+
+ private class ChromaArea : CompositeDrawable
+ {
+ [Resolved]
+ private LadderInfo ladder { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ // chroma key area for stable gameplay
+ Colour = new Color4(0, 255, 0, 255);
+
+ ladder.PlayersPerTeam.BindValueChanged(performLayout, true);
+ }
+
+ private void performLayout(ValueChangedEvent playerCount)
+ {
+ switch (playerCount.NewValue)
+ {
+ case 3:
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.5f,
+ Height = 0.5f,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ },
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Height = 0.5f,
+ },
+ };
+ break;
+
+ default:
+ InternalChild = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ };
+ break;
+ }
+ }
+ }
}
}
diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
index d809dfc994..9785b7e647 100644
--- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
+++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
@@ -2,15 +2,42 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Tournament.Components;
+using osu.Framework.Graphics.Shapes;
+using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Showcase
{
- public class ShowcaseScreen : BeatmapInfoScreen
+ public class ShowcaseScreen : BeatmapInfoScreen // IProvideVideo
{
[BackgroundDependencyLoader]
private void load()
{
- AddInternal(new TournamentLogo());
+ AddRangeInternal(new Drawable[]
+ {
+ new TournamentLogo(),
+ new TourneyVideo("showcase")
+ {
+ Loop = true,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Container
+ {
+ Padding = new MarginPadding { Bottom = SongBar.HEIGHT },
+ RelativeSizeAxes = Axes.Both,
+ Child = new Box
+ {
+ // chroma key area for stable gameplay
+ Name = "chroma",
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.Both,
+ Colour = new Color4(0, 255, 0, 255),
+ }
+ }
+ });
}
}
}
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 90c100db05..3860f12baa 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -149,6 +149,11 @@ namespace osu.Game.Beatmaps
}
}
+ public string[] SearchableTerms => new[]
+ {
+ Version
+ }.Concat(Metadata?.SearchableTerms ?? Enumerable.Empty()).Where(s => !string.IsNullOrEmpty(s)).ToArray();
+
public override string ToString()
{
string version = string.IsNullOrEmpty(Version) ? string.Empty : $"[{Version}]";
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 6542866936..34ad1df6bc 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -17,7 +17,6 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Lists;
using osu.Framework.Logging;
using osu.Framework.Platform;
-using osu.Framework.Threading;
using osu.Game.Beatmaps.Formats;
using osu.Game.Database;
using osu.Game.IO;
@@ -61,7 +60,7 @@ namespace osu.Game.Beatmaps
private readonly BeatmapStore beatmaps;
private readonly AudioManager audioManager;
private readonly GameHost host;
- private readonly BeatmapUpdateQueue updateQueue;
+ private readonly BeatmapOnlineLookupQueue onlineLookupQueue;
private readonly Storage exportStorage;
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null,
@@ -78,7 +77,7 @@ namespace osu.Game.Beatmaps
beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
- updateQueue = new BeatmapUpdateQueue(api);
+ onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
exportStorage = storage.GetStorageForDirectory("exports");
}
@@ -105,7 +104,7 @@ namespace osu.Game.Beatmaps
bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
- await updateQueue.UpdateAsync(beatmapSet, cancellationToken);
+ await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken);
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
@@ -141,7 +140,7 @@ namespace osu.Game.Beatmaps
{
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
- LogForModel(beatmapSet, "Validating online IDs...");
+ LogForModel(beatmapSet, $"Validating online IDs for {beatmapSet.Beatmaps.Count} beatmaps...");
// ensure all IDs are unique
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
@@ -246,6 +245,12 @@ namespace osu.Game.Beatmaps
if (beatmapInfo?.BeatmapSet == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo)
return DefaultBeatmap;
+ if (beatmapInfo.BeatmapSet.Files == null)
+ {
+ var info = beatmapInfo;
+ beatmapInfo = QueryBeatmap(b => b.ID == info.ID);
+ }
+
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
@@ -287,13 +292,37 @@ namespace osu.Game.Beatmaps
/// Returns a list of all usable s.
///
/// A list of available .
- public List GetAllUsableBeatmapSets() => GetAllUsableBeatmapSetsEnumerable().ToList();
+ public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All) => GetAllUsableBeatmapSetsEnumerable(includes).ToList();
///
- /// Returns a list of all usable s.
+ /// Returns a list of all usable s. Note that files are not populated.
///
+ /// The level of detail to include in the returned objects.
/// A list of available .
- public IQueryable GetAllUsableBeatmapSetsEnumerable() => beatmaps.ConsumableItems.Where(s => !s.DeletePending && !s.Protected);
+ public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes)
+ {
+ IQueryable queryable;
+
+ switch (includes)
+ {
+ case IncludedDetails.Minimal:
+ queryable = beatmaps.BeatmapSetsOverview;
+ break;
+
+ case IncludedDetails.AllButFiles:
+ queryable = beatmaps.BeatmapSetsWithoutFiles;
+ break;
+
+ default:
+ queryable = beatmaps.ConsumableItems;
+ break;
+ }
+
+ // AsEnumerable used here to avoid applying the WHERE in sql. When done so, ef core 2.x uses an incorrect ORDER BY
+ // clause which causes queries to take 5-10x longer.
+ // TODO: remove if upgrading to EF core 3.x.
+ return queryable.AsEnumerable().Where(s => !s.DeletePending && !s.Protected);
+ }
///
/// Perform a lookup query on available s.
@@ -352,7 +381,7 @@ namespace osu.Game.Beatmaps
foreach (var file in files.Where(f => f.Filename.EndsWith(".osu")))
{
using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath))
- using (var ms = new MemoryStream()) //we need a memory stream so we can seek
+ using (var ms = new MemoryStream()) // we need a memory stream so we can seek
using (var sr = new LineBufferedReader(ms))
{
raw.CopyTo(ms);
@@ -416,70 +445,26 @@ namespace osu.Game.Beatmaps
protected override Texture GetBackground() => null;
protected override Track GetTrack() => null;
}
+ }
- private class BeatmapUpdateQueue
- {
- private readonly IAPIProvider api;
+ ///
+ /// The level of detail to include in database results.
+ ///
+ public enum IncludedDetails
+ {
+ ///
+ /// Only include beatmap difficulties and set level metadata.
+ ///
+ Minimal,
- private const int update_queue_request_concurrency = 4;
+ ///
+ /// Include all difficulties, rulesets, difficulty metadata but no files.
+ ///
+ AllButFiles,
- private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(update_queue_request_concurrency, nameof(BeatmapUpdateQueue));
-
- public BeatmapUpdateQueue(IAPIProvider api)
- {
- this.api = api;
- }
-
- public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken)
- {
- if (api?.State != APIState.Online)
- return Task.CompletedTask;
-
- LogForModel(beatmapSet, "Performing online lookups...");
- return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray());
- }
-
- // todo: expose this when we need to do individual difficulty lookups.
- protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken)
- => Task.Factory.StartNew(() => update(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler, updateScheduler);
-
- private void update(BeatmapSetInfo set, BeatmapInfo beatmap)
- {
- if (api?.State != APIState.Online)
- return;
-
- var req = new GetBeatmapRequest(beatmap);
-
- req.Failure += fail;
-
- try
- {
- // intentionally blocking to limit web request concurrency
- api.Perform(req);
-
- var res = req.Result;
-
- if (res != null)
- {
- beatmap.Status = res.Status;
- beatmap.BeatmapSet.Status = res.BeatmapSet.Status;
- beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
- beatmap.OnlineBeatmapID = res.OnlineBeatmapID;
-
- LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.");
- }
- }
- catch (Exception e)
- {
- fail(e);
- }
-
- void fail(Exception e)
- {
- beatmap.OnlineBeatmapID = null;
- LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})");
- }
- }
- }
+ ///
+ /// Include everything.
+ ///
+ All
}
}
diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
new file mode 100644
index 0000000000..2c79a664c5
--- /dev/null
+++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
@@ -0,0 +1,195 @@
+// 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.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Dapper;
+using Microsoft.Data.Sqlite;
+using osu.Framework.Development;
+using osu.Framework.IO.Network;
+using osu.Framework.Logging;
+using osu.Framework.Platform;
+using osu.Framework.Threading;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using SharpCompress.Compressors;
+using SharpCompress.Compressors.BZip2;
+
+namespace osu.Game.Beatmaps
+{
+ public partial class BeatmapManager
+ {
+ private class BeatmapOnlineLookupQueue
+ {
+ private readonly IAPIProvider api;
+ private readonly Storage storage;
+
+ private const int update_queue_request_concurrency = 4;
+
+ private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(update_queue_request_concurrency, nameof(BeatmapOnlineLookupQueue));
+
+ private FileWebRequest cacheDownloadRequest;
+
+ private const string cache_database_name = "online.db";
+
+ public BeatmapOnlineLookupQueue(IAPIProvider api, Storage storage)
+ {
+ this.api = api;
+ this.storage = storage;
+
+ // avoid downloading / using cache for unit tests.
+ if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name))
+ prepareLocalCache();
+ }
+
+ public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken)
+ {
+ if (api?.State != APIState.Online)
+ return Task.CompletedTask;
+
+ LogForModel(beatmapSet, "Performing online lookups...");
+ return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray());
+ }
+
+ // todo: expose this when we need to do individual difficulty lookups.
+ protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken)
+ => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler, updateScheduler);
+
+ private void lookup(BeatmapSetInfo set, BeatmapInfo beatmap)
+ {
+ if (checkLocalCache(set, beatmap))
+ return;
+
+ if (api?.State != APIState.Online)
+ return;
+
+ var req = new GetBeatmapRequest(beatmap);
+
+ req.Failure += fail;
+
+ try
+ {
+ // intentionally blocking to limit web request concurrency
+ api.Perform(req);
+
+ var res = req.Result;
+
+ if (res != null)
+ {
+ beatmap.Status = res.Status;
+ beatmap.BeatmapSet.Status = res.BeatmapSet.Status;
+ beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
+ beatmap.OnlineBeatmapID = res.OnlineBeatmapID;
+
+ LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.");
+ }
+ }
+ catch (Exception e)
+ {
+ fail(e);
+ }
+
+ void fail(Exception e)
+ {
+ beatmap.OnlineBeatmapID = null;
+ LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})");
+ }
+ }
+
+ private void prepareLocalCache()
+ {
+ string cacheFilePath = storage.GetFullPath(cache_database_name);
+ string compressedCacheFilePath = $"{cacheFilePath}.bz2";
+
+ cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2");
+
+ cacheDownloadRequest.Failed += ex =>
+ {
+ File.Delete(compressedCacheFilePath);
+ File.Delete(cacheFilePath);
+
+ Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache download failed: {ex}", LoggingTarget.Database);
+ };
+
+ cacheDownloadRequest.Finished += () =>
+ {
+ try
+ {
+ using (var stream = File.OpenRead(cacheDownloadRequest.Filename))
+ using (var outStream = File.OpenWrite(cacheFilePath))
+ using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false))
+ bz2.CopyTo(outStream);
+
+ // set to null on completion to allow lookups to begin using the new source
+ cacheDownloadRequest = null;
+ }
+ catch (Exception ex)
+ {
+ Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache extraction failed: {ex}", LoggingTarget.Database);
+ File.Delete(cacheFilePath);
+ }
+ finally
+ {
+ File.Delete(compressedCacheFilePath);
+ }
+ };
+
+ cacheDownloadRequest.PerformAsync();
+ }
+
+ private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmap)
+ {
+ // download is in progress (or was, and failed).
+ if (cacheDownloadRequest != null)
+ return false;
+
+ // database is unavailable.
+ if (!storage.Exists(cache_database_name))
+ return false;
+
+ try
+ {
+ using (var db = new SqliteConnection(storage.GetDatabaseConnectionString("online")))
+ {
+ var found = db.QuerySingleOrDefault(
+ "SELECT * FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path", beatmap);
+
+ if (found != null)
+ {
+ var status = (BeatmapSetOnlineStatus)found.approved;
+
+ beatmap.Status = status;
+ beatmap.BeatmapSet.Status = status;
+ beatmap.BeatmapSet.OnlineBeatmapSetID = found.beatmapset_id;
+ beatmap.OnlineBeatmapID = found.beatmap_id;
+
+ LogForModel(set, $"Cached local retrieval for {beatmap}.");
+ return true;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}.");
+ }
+
+ return false;
+ }
+
+ [Serializable]
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private class CachedOnlineBeatmapLookup
+ {
+ public int approved { get; set; }
+
+ public int? beatmapset_id { get; set; }
+
+ public int? beatmap_id { get; set; }
+ }
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs
index a2279fdb14..642bafd2ac 100644
--- a/osu.Game/Beatmaps/BeatmapStore.cs
+++ b/osu.Game/Beatmaps/BeatmapStore.cs
@@ -87,6 +87,18 @@ namespace osu.Game.Beatmaps
base.Purge(items, context);
}
+ public IQueryable BeatmapSetsOverview => ContextFactory.Get().BeatmapSetInfo
+ .Include(s => s.Metadata)
+ .Include(s => s.Beatmaps)
+ .AsNoTracking();
+
+ public IQueryable BeatmapSetsWithoutFiles => ContextFactory.Get().BeatmapSetInfo
+ .Include(s => s.Metadata)
+ .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset)
+ .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
+ .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
+ .AsNoTracking();
+
public IQueryable Beatmaps =>
ContextFactory.Get().BeatmapInfo
.Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata)
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index 44ccbb350d..7727f25967 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -13,6 +13,7 @@ using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
+using osuTK;
namespace osu.Game.Beatmaps.Formats
{
@@ -124,7 +125,12 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"CircleSize: {beatmap.BeatmapInfo.BaseDifficulty.CircleSize}"));
writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty}"));
writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.BeatmapInfo.BaseDifficulty.ApproachRate}"));
- writer.WriteLine(FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}"));
+
+ // Taiko adjusts the slider multiplier (see: TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER)
+ writer.WriteLine(beatmap.BeatmapInfo.RulesetID == 1
+ ? FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / 1.4f}")
+ : FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}"));
+
writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate}"));
}
@@ -197,51 +203,63 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[HitObjects]");
- // TODO: implement other legacy rulesets
+ foreach (var h in beatmap.HitObjects)
+ handleHitObject(writer, h);
+ }
+
+ private void handleHitObject(TextWriter writer, HitObject hitObject)
+ {
+ Vector2 position = new Vector2(256, 192);
+
switch (beatmap.BeatmapInfo.RulesetID)
{
case 0:
- foreach (var h in beatmap.HitObjects)
- handleOsuHitObject(writer, h);
+ position = ((IHasPosition)hitObject).Position;
+ break;
+
+ case 2:
+ position.X = ((IHasXPosition)hitObject).X * 512;
+ break;
+
+ case 3:
+ int totalColumns = (int)Math.Max(1, beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
+ position.X = (int)Math.Ceiling(((IHasXPosition)hitObject).X * (512f / totalColumns));
break;
}
- }
- private void handleOsuHitObject(TextWriter writer, HitObject hitObject)
- {
- var positionData = (IHasPosition)hitObject;
-
- writer.Write(FormattableString.Invariant($"{positionData.X},"));
- writer.Write(FormattableString.Invariant($"{positionData.Y},"));
+ writer.Write(FormattableString.Invariant($"{position.X},"));
+ writer.Write(FormattableString.Invariant($"{position.Y},"));
writer.Write(FormattableString.Invariant($"{hitObject.StartTime},"));
writer.Write(FormattableString.Invariant($"{(int)getObjectType(hitObject)},"));
-
- writer.Write(hitObject is IHasCurve
- ? FormattableString.Invariant($"0,")
- : FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},"));
+ writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},"));
if (hitObject is IHasCurve curveData)
{
- addCurveData(writer, curveData, positionData);
+ addCurveData(writer, curveData, position);
writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true));
}
else
{
- if (hitObject is IHasEndTime endTimeData)
- writer.Write(FormattableString.Invariant($"{endTimeData.EndTime},"));
+ if (hitObject is IHasEndTime)
+ addEndTimeData(writer, hitObject);
+
writer.Write(getSampleBank(hitObject.Samples));
}
writer.WriteLine();
}
- private static LegacyHitObjectType getObjectType(HitObject hitObject)
+ private LegacyHitObjectType getObjectType(HitObject hitObject)
{
- var comboData = (IHasCombo)hitObject;
+ LegacyHitObjectType type = 0;
- var type = (LegacyHitObjectType)(comboData.ComboOffset << 4);
+ if (hitObject is IHasCombo combo)
+ {
+ type = (LegacyHitObjectType)(combo.ComboOffset << 4);
- if (comboData.NewCombo) type |= LegacyHitObjectType.NewCombo;
+ if (combo.NewCombo)
+ type |= LegacyHitObjectType.NewCombo;
+ }
switch (hitObject)
{
@@ -250,7 +268,10 @@ namespace osu.Game.Beatmaps.Formats
break;
case IHasEndTime _:
- type |= LegacyHitObjectType.Spinner;
+ if (beatmap.BeatmapInfo.RulesetID == 3)
+ type |= LegacyHitObjectType.Hold;
+ else
+ type |= LegacyHitObjectType.Spinner;
break;
default:
@@ -261,7 +282,7 @@ namespace osu.Game.Beatmaps.Formats
return type;
}
- private void addCurveData(TextWriter writer, IHasCurve curveData, IHasPosition positionData)
+ private void addCurveData(TextWriter writer, IHasCurve curveData, Vector2 position)
{
PathType? lastType = null;
@@ -297,13 +318,13 @@ namespace osu.Game.Beatmaps.Formats
else
{
// New segment with the same type - duplicate the control point
- writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}|"));
+ writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}|"));
}
}
if (i != 0)
{
- writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}"));
+ writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}"));
writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ",");
}
}
@@ -324,6 +345,20 @@ namespace osu.Game.Beatmaps.Formats
}
}
+ private void addEndTimeData(TextWriter writer, HitObject hitObject)
+ {
+ var endTimeData = (IHasEndTime)hitObject;
+ var type = getObjectType(hitObject);
+
+ char suffix = ',';
+
+ // Holds write the end time as if it's part of sample data.
+ if (type == LegacyHitObjectType.Hold)
+ suffix = ':';
+
+ writer.Write(FormattableString.Invariant($"{endTimeData.EndTime}{suffix}"));
+ }
+
private string getSampleBank(IList samples, bool banksOnly = false, bool zeroBanks = false)
{
LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank);
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index f30340956a..d2804bdc05 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -197,7 +197,7 @@ namespace osu.Game.Beatmaps
public override string ToString() => BeatmapInfo.ToString();
- public bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
+ public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
public IBeatmap Beatmap
{
@@ -233,7 +233,7 @@ namespace osu.Game.Beatmaps
protected abstract Texture GetBackground();
private readonly RecyclableLazy background;
- public bool TrackLoaded => track.IsResultAvailable;
+ public virtual bool TrackLoaded => track.IsResultAvailable;
public Track Track => track.Value;
protected abstract Track GetTrack();
private RecyclableLazy
protected abstract string[] HashableFileTypes { get; }
- protected static void LogForModel(TModel model, string message, Exception e = null)
+ internal static void LogForModel(TModel model, string message, Exception e = null)
{
string prefix = $"[{(model?.Hash ?? "?????").Substring(0, 5)}]";
diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs
index 590e4b2a5c..27027202ce 100644
--- a/osu.Game/Graphics/Backgrounds/Triangles.cs
+++ b/osu.Game/Graphics/Backgrounds/Triangles.cs
@@ -193,8 +193,8 @@ namespace osu.Game.Graphics.Backgrounds
float u1 = 1 - RNG.NextSingle(); //uniform(0,1] random floats
float u2 = 1 - RNG.NextSingle();
- 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)
+ 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)
return new TriangleParticle { Scale = scale };
}
diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs
index 1824fcd878..d504a11b22 100644
--- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs
@@ -158,7 +158,7 @@ namespace osu.Game.Graphics.Containers
{
if (!base.OnMouseDown(e)) return false;
- //note that we are changing the colour of the box here as to not interfere with the hover effect.
+ // note that we are changing the colour of the box here as to not interfere with the hover effect.
box.FadeColour(highlightColour, 100);
return true;
}
diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs
index 8c520f4e10..8b6df4a834 100644
--- a/osu.Game/Graphics/DrawableDate.cs
+++ b/osu.Game/Graphics/DrawableDate.cs
@@ -139,7 +139,7 @@ namespace osu.Game.Graphics
return false;
dateText.Text = $"{date:d MMMM yyyy} ";
- timeText.Text = $"{date:hh:mm:ss \"UTC\"z}";
+ timeText.Text = $"{date:HH:mm:ss \"UTC\"z}";
return true;
}
diff --git a/osu.Game/IPC/ArchiveImportIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs
index 484db932f8..029908ec9d 100644
--- a/osu.Game/IPC/ArchiveImportIPCChannel.cs
+++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs
@@ -32,7 +32,7 @@ namespace osu.Game.IPC
{
if (importer == null)
{
- //we want to contact a remote osu! to handle the import.
+ // we want to contact a remote osu! to handle the import.
await SendMessageAsync(new ArchiveImportMessage { Path = path });
return;
}
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index adfef1d11f..4945f7f185 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -127,7 +127,7 @@ namespace osu.Game.Online.API
case APIState.Offline:
case APIState.Connecting:
- //work to restore a connection...
+ // work to restore a connection...
if (!HasLogin)
{
State = APIState.Offline;
@@ -180,7 +180,7 @@ namespace osu.Game.Online.API
break;
}
- //hard bail if we can't get a valid access token.
+ // hard bail if we can't get a valid access token.
if (authentication.RequestAccessToken() == null)
{
Logout();
@@ -274,7 +274,7 @@ namespace osu.Game.Online.API
{
req.Perform(this);
- //we could still be in initialisation, at which point we don't want to say we're Online yet.
+ // we could still be in initialisation, at which point we don't want to say we're Online yet.
if (IsLoggedIn) State = APIState.Online;
failureCount = 0;
@@ -339,7 +339,7 @@ namespace osu.Game.Online.API
log.Add($@"API failure count is now {failureCount}");
if (failureCount < 3)
- //we might try again at an api level.
+ // we might try again at an api level.
return false;
if (State == APIState.Online)
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index 0bba04cac3..0f8acbb7af 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -98,7 +98,7 @@ namespace osu.Game.Online.API
if (checkAndScheduleFailure())
return;
- if (!WebRequest.Aborted) //could have been aborted by a Cancel() call
+ if (!WebRequest.Aborted) // could have been aborted by a Cancel() call
{
Logger.Log($@"Performing request {this}", LoggingTarget.Network);
WebRequest.Perform();
diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs
index 6f67a95f53..dbb2da5c03 100644
--- a/osu.Game/Online/Chat/Channel.cs
+++ b/osu.Game/Online/Chat/Channel.cs
@@ -61,7 +61,7 @@ namespace osu.Game.Online.Chat
///
public event Action MessageRemoved;
- public bool ReadOnly => false; //todo not yet used.
+ public bool ReadOnly => false; // todo: not yet used.
public override string ToString() => Name;
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 822f628dd2..53872ddcba 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -93,6 +93,12 @@ namespace osu.Game.Online.Chat
{
if (!(e.NewValue is ChannelSelectorTabItem.ChannelSelectorTabChannel))
JoinChannel(e.NewValue);
+
+ if (e.NewValue?.MessagesLoaded == false)
+ {
+ // let's fetch a small number of messages to bring us up-to-date with the backlog.
+ fetchInitalMessages(e.NewValue);
+ }
}
///
@@ -375,12 +381,6 @@ namespace osu.Game.Online.Chat
if (CurrentChannel.Value == null)
CurrentChannel.Value = channel;
- if (!channel.MessagesLoaded)
- {
- // let's fetch a small number of messages to bring us up-to-date with the backlog.
- fetchInitalMessages(channel);
- }
-
return channel;
}
diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs
index 717de18c14..6af2561c89 100644
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -78,13 +78,13 @@ namespace osu.Game.Online.Chat
{
result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText);
- //since we just changed the line display text, offset any already processed links.
+ // since we just changed the line display text, offset any already processed links.
result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0);
var details = GetLinkDetails(linkText);
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument));
- //adjust the offset for processing the current matches group.
+ // adjust the offset for processing the current matches group.
captureOffset += m.Length - displayText.Length;
}
}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index f5f7d0cef4..3caffb6db5 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -18,6 +18,7 @@ using osu.Game.Screens.Menu;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Development;
@@ -97,6 +98,7 @@ namespace osu.Game
private MainMenu menuScreen;
+ [CanBeNull]
private IntroScreen introScreen;
private Bindable configRuleset;
@@ -609,7 +611,7 @@ namespace osu.Game
loadComponentSingleFile(screenshotManager, Add);
- //overlay elements
+ // overlay elements
loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true);
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
@@ -781,7 +783,7 @@ namespace osu.Game
{
var previousLoadStream = asyncLoadStream;
- //chain with existing load stream
+ // chain with existing load stream
asyncLoadStream = Task.Run(async () =>
{
if (previousLoadStream != null)
@@ -914,10 +916,7 @@ namespace osu.Game
if (ScreenStack.CurrentScreen is Loader)
return false;
- if (introScreen == null)
- return true;
-
- if (!introScreen.DidLoadMenu || !(ScreenStack.CurrentScreen is IntroScreen))
+ if (introScreen?.DidLoadMenu == true && !(ScreenStack.CurrentScreen is IntroScreen))
{
Scheduler.Add(introScreen.MakeCurrent);
return true;
diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs
index 589f2d5072..67782dfe3f 100644
--- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs
@@ -50,13 +50,6 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
[BackgroundDependencyLoader(true)]
private void load(OsuGame game, BeatmapManager beatmaps, OsuConfigManager osuConfig)
{
- if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false)
- {
- button.Enabled.Value = false;
- button.TooltipText = "this beatmap is currently not available for download.";
- return;
- }
-
noVideoSetting = osuConfig.GetBindable(OsuSetting.PreferNoVideo);
button.Action = () =>
@@ -81,6 +74,26 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
break;
}
};
+
+ State.BindValueChanged(state =>
+ {
+ switch (state.NewValue)
+ {
+ case DownloadState.LocallyAvailable:
+ button.Enabled.Value = true;
+ button.TooltipText = string.Empty;
+ break;
+
+ default:
+ if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false)
+ {
+ button.Enabled.Value = false;
+ button.TooltipText = "this beatmap is currently not available for download.";
+ }
+
+ break;
+ }
+ }, true);
}
}
}
diff --git a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
index 84d35da096..28c36e6c56 100644
--- a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
: base(beatmap)
{
Width = 380;
- Height = 140 + vertical_padding; //full height of all the elements plus vertical padding (autosize uses the image)
+ Height = 140 + vertical_padding; // full height of all the elements plus vertical padding (autosize uses the image)
}
protected override void LoadComplete()
diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs
index 17fa689cd2..06e31277dd 100644
--- a/osu.Game/Overlays/BeatmapSet/Header.cs
+++ b/osu.Game/Overlays/BeatmapSet/Header.cs
@@ -140,7 +140,7 @@ namespace osu.Game.Overlays.BeatmapSet
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
- Margin = new MarginPadding { Left = 3, Bottom = 4 }, //To better lineup with the font
+ Margin = new MarginPadding { Left = 3, Bottom = 4 }, // To better lineup with the font
},
}
},
@@ -264,7 +264,7 @@ namespace osu.Game.Overlays.BeatmapSet
{
if (BeatmapSet.Value == null) return;
- if (BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false)
+ if ((BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) && State.Value != DownloadState.LocallyAvailable)
{
downloadButtonsContainer.Clear();
return;
diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs
index 6019657cf0..d63faebae4 100644
--- a/osu.Game/Overlays/Chat/DrawableChannel.cs
+++ b/osu.Game/Overlays/Chat/DrawableChannel.cs
@@ -105,6 +105,14 @@ namespace osu.Game.Overlays.Chat
private void newMessagesArrived(IEnumerable newMessages)
{
+ if (newMessages.Min(m => m.Id) < chatLines.Max(c => c.Message.Id))
+ {
+ // there is a case (on initial population) that we may receive past messages and need to reorder.
+ // easiest way is to just combine messages and recreate drawables (less worrying about day separators etc.)
+ newMessages = newMessages.Concat(chatLines.Select(c => c.Message)).OrderBy(m => m.Id).ToList();
+ ChatLineFlow.Clear();
+ }
+
bool shouldScrollToEnd = scroll.IsScrolledToEnd(10) || !chatLines.Any() || newMessages.Any(m => m is LocalMessage);
// Add up to last Channel.MAX_HISTORY messages
diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs
index 34afc3c431..5ba55f6d45 100644
--- a/osu.Game/Overlays/ChatOverlay.cs
+++ b/osu.Game/Overlays/ChatOverlay.cs
@@ -358,7 +358,7 @@ namespace osu.Game.Overlays
protected override void OnFocus(FocusEvent e)
{
- //this is necessary as textbox is masked away and therefore can't get focus :(
+ // this is necessary as textbox is masked away and therefore can't get focus :(
textbox.TakeFocus();
base.OnFocus(e);
}
diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs
index 591a9dc86e..e7bfeaf968 100644
--- a/osu.Game/Overlays/Comments/CommentsContainer.cs
+++ b/osu.Game/Overlays/Comments/CommentsContainer.cs
@@ -153,7 +153,7 @@ namespace osu.Game.Overlays.Comments
request?.Cancel();
loadCancellation?.Cancel();
request = new GetCommentsRequest(id.Value, type.Value, Sort.Value, currentPage++, 0);
- request.Success += onSuccess;
+ request.Success += res => Schedule(() => onSuccess(res));
api.PerformAsync(request);
}
diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs
index 59d748bc5d..9f9dbdbaf1 100644
--- a/osu.Game/Overlays/DialogOverlay.cs
+++ b/osu.Game/Overlays/DialogOverlay.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Overlays
{
if (v != Visibility.Hidden) return;
- //handle the dialog being dismissed.
+ // handle the dialog being dismissed.
dialog.Delay(PopupDialog.EXIT_DURATION).Expire();
if (dialog == CurrentDialog)
diff --git a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs
index 56e93b6a1e..5b44c486a3 100644
--- a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs
+++ b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.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 osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osu.Game.Overlays.Settings;
@@ -9,7 +10,11 @@ namespace osu.Game.Overlays.KeyBinding
{
public class GlobalKeyBindingsSection : SettingsSection
{
- public override IconUsage Icon => FontAwesome.Solid.Globe;
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Globe
+ };
+
public override string Header => "Global";
public GlobalKeyBindingsSection(GlobalActionContainer manager)
diff --git a/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs b/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs
index 1f4042c57c..332fb6c8fc 100644
--- a/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs
+++ b/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.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 osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Overlays.Settings;
@@ -10,7 +11,11 @@ namespace osu.Game.Overlays.KeyBinding
{
public class RulesetBindingsSection : SettingsSection
{
- public override IconUsage Icon => (ruleset.CreateInstance().CreateIcon() as SpriteIcon)?.Icon ?? OsuIcon.Hot;
+ public override Drawable CreateIcon() => ruleset?.CreateInstance()?.CreateIcon() ?? new SpriteIcon
+ {
+ Icon = OsuIcon.Hot
+ };
+
public override string Header => ruleset.Name;
private readonly RulesetInfo ruleset;
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index e9b3598625..3d0ad1a594 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -37,7 +37,6 @@ namespace osu.Game.Overlays.Mods
protected readonly TriangleButton CloseButton;
protected readonly OsuSpriteText MultiplierLabel;
- protected readonly OsuSpriteText UnrankedLabel;
protected override bool BlockNonPositionalInput => false;
@@ -57,6 +56,8 @@ namespace osu.Game.Overlays.Mods
protected Color4 HighMultiplierColour;
private const float content_width = 0.8f;
+ private const float footer_button_spacing = 20;
+
private readonly FillFlowContainer footerContainer;
private SampleChannel sampleOn, sampleOff;
@@ -103,7 +104,7 @@ namespace osu.Game.Overlays.Mods
{
new Dimension(GridSizeMode.Absolute, 90),
new Dimension(GridSizeMode.Distributed),
- new Dimension(GridSizeMode.Absolute, 70),
+ new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
@@ -197,7 +198,8 @@ namespace osu.Game.Overlays.Mods
// Footer
new Container
{
- RelativeSizeAxes = Axes.Both,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Children = new Drawable[]
@@ -215,7 +217,9 @@ namespace osu.Game.Overlays.Mods
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Width = content_width,
- Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(footer_button_spacing, footer_button_spacing / 2),
+ LayoutDuration = 100,
+ LayoutEasing = Easing.OutQuint,
Padding = new MarginPadding
{
Vertical = 15,
@@ -228,10 +232,8 @@ namespace osu.Game.Overlays.Mods
Width = 180,
Text = "Deselect All",
Action = DeselectAll,
- Margin = new MarginPadding
- {
- Right = 20
- }
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
},
CustomiseButton = new TriangleButton
{
@@ -239,49 +241,41 @@ namespace osu.Game.Overlays.Mods
Text = "Customisation",
Action = () => ModSettingsContainer.Alpha = ModSettingsContainer.Alpha == 1 ? 0 : 1,
Enabled = { Value = false },
- Margin = new MarginPadding
- {
- Right = 20
- }
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
},
CloseButton = new TriangleButton
{
Width = 180,
Text = "Close",
Action = Hide,
- Margin = new MarginPadding
- {
- Right = 20
- }
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
},
- new OsuSpriteText
+ new FillFlowContainer
{
- Text = @"Score Multiplier:",
- Font = OsuFont.GetFont(size: 30),
- Margin = new MarginPadding
+ AutoSizeAxes = Axes.Both,
+ Spacing = new Vector2(footer_button_spacing / 2, 0),
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
+ Children = new Drawable[]
{
- Top = 5,
- Right = 10
- }
+ new OsuSpriteText
+ {
+ Text = @"Score Multiplier:",
+ Font = OsuFont.GetFont(size: 30),
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
+ },
+ MultiplierLabel = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold),
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
+ Width = 70, // make width fixed so reflow doesn't occur when multiplier number changes.
+ },
+ },
},
- MultiplierLabel = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold),
- Margin = new MarginPadding
- {
- Top = 5
- }
- },
- UnrankedLabel = new OsuSpriteText
- {
- Text = @"(Unranked)",
- Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold),
- Margin = new MarginPadding
- {
- Top = 5,
- Left = 10
- }
- }
}
}
},
@@ -327,7 +321,6 @@ namespace osu.Game.Overlays.Mods
{
LowMultiplierColour = colours.Red;
HighMultiplierColour = colours.Green;
- UnrankedLabel.Colour = colours.Blue;
availableMods = osu.AvailableMods.GetBoundCopy();
@@ -431,12 +424,10 @@ namespace osu.Game.Overlays.Mods
private void updateMods()
{
var multiplier = 1.0;
- var ranked = true;
foreach (var mod in SelectedMods.Value)
{
multiplier *= mod.ScoreMultiplier;
- ranked &= mod.Ranked;
}
MultiplierLabel.Text = $"{multiplier:N2}x";
@@ -446,8 +437,6 @@ namespace osu.Game.Overlays.Mods
MultiplierLabel.FadeColour(LowMultiplierColour, 200);
else
MultiplierLabel.FadeColour(Color4.White, 200);
-
- UnrankedLabel.FadeTo(ranked ? 0 : 1, 200);
}
private void updateModSettings(ValueChangedEvent> selectedMods)
diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs
index de2f916946..840fa51b4f 100644
--- a/osu.Game/Overlays/Music/PlaylistItem.cs
+++ b/osu.Game/Overlays/Music/PlaylistItem.cs
@@ -78,7 +78,7 @@ namespace osu.Game.Overlays.Music
{
text.Clear();
- //space after the title to put a space between the title and artist
+ // space after the title to put a space between the title and artist
titleSprites = text.AddText(title.Value + @" ", sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular)).OfType();
text.AddText(artist.Value, sprite =>
diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index d788929739..ded641b262 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Overlays
beatmaps.ItemAdded += handleBeatmapAdded;
beatmaps.ItemRemoved += handleBeatmapRemoved;
- beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()));
+ beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal).OrderBy(_ => RNG.Next()));
}
protected override void LoadComplete()
@@ -172,10 +172,15 @@ namespace osu.Game.Overlays
}
///
- /// Play the previous track or restart the current track if it's current time below
+ /// Play the previous track or restart the current track if it's current time below .
///
- /// The that indicate the decided action
- public PreviousTrackResult PreviousTrack()
+ public void PreviousTrack() => Schedule(() => prev());
+
+ ///
+ /// Play the previous track or restart the current track if it's current time below .
+ ///
+ /// The that indicate the decided action.
+ private PreviousTrackResult prev()
{
var currentTrackPosition = current?.Track.CurrentTime;
@@ -204,8 +209,7 @@ namespace osu.Game.Overlays
///
/// Play the next random or playlist track.
///
- /// Whether the operation was successful.
- public bool NextTrack() => next();
+ public void NextTrack() => Schedule(() => next());
private bool next(bool instant = false)
{
@@ -246,7 +250,7 @@ namespace osu.Game.Overlays
}
else
{
- //figure out the best direction based on order in playlist.
+ // figure out the best direction based on order in playlist.
var last = BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo?.ID).Count();
var next = beatmap.NewValue == null ? -1 : BeatmapSets.TakeWhile(b => b.ID != beatmap.NewValue.BeatmapSetInfo?.ID).Count();
@@ -319,13 +323,13 @@ namespace osu.Game.Overlays
return true;
case GlobalAction.MusicNext:
- if (NextTrack())
+ if (next())
onScreenDisplay?.Display(new MusicControllerToast("Next track"));
return true;
case GlobalAction.MusicPrev:
- switch (PreviousTrack())
+ switch (prev())
{
case PreviousTrackResult.Restart:
onScreenDisplay?.Display(new MusicControllerToast("Restart track"));
diff --git a/osu.Game/Overlays/News/NewsArticleCover.cs b/osu.Game/Overlays/News/NewsArticleCover.cs
index cca0cfb4a0..e3f5a8cea3 100644
--- a/osu.Game/Overlays/News/NewsArticleCover.cs
+++ b/osu.Game/Overlays/News/NewsArticleCover.cs
@@ -162,7 +162,7 @@ namespace osu.Game.Overlays.News
public string TooltipText => date.ToString("dddd dd MMMM yyyy hh:mm:ss UTCz").ToUpper();
}
- //fake API data struct to use for now as a skeleton for data, as there is no API struct for news article info for now
+ // fake API data struct to use for now as a skeleton for data, as there is no API struct for news article info for now
public class ArticleInfo
{
public string Title { get; set; }
diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs
index 99836705c4..3105ecd742 100644
--- a/osu.Game/Overlays/Notifications/ProgressNotification.cs
+++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Notifications
{
base.LoadComplete();
- //we may have received changes before we were displayed.
+ // we may have received changes before we were displayed.
updateState();
}
diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs
index 118cb037cb..ebb4a96d14 100644
--- a/osu.Game/Overlays/NowPlayingOverlay.cs
+++ b/osu.Game/Overlays/NowPlayingOverlay.cs
@@ -261,7 +261,7 @@ namespace osu.Game.Overlays
// todo: this can likely be replaced with WorkingBeatmap.GetBeatmapAsync()
Task.Run(() =>
{
- if (beatmap?.Beatmap == null) //this is not needed if a placeholder exists
+ if (beatmap?.Beatmap == null) // this is not needed if a placeholder exists
{
title.Text = @"Nothing to play";
artist.Text = @"Nothing to play";
diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs
index 5d36cac20e..1497ca8fa8 100644
--- a/osu.Game/Overlays/OSD/Toast.cs
+++ b/osu.Game/Overlays/OSD/Toast.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Overlays.OSD
InternalChildren = new Drawable[]
{
- new Container //this container exists just to set a minimum width for the toast
+ new Container // this container exists just to set a minimum width for the toast
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game/Overlays/Profile/Header/Components/OverlinedInfoContainer.cs b/osu.Game/Overlays/Profile/Header/Components/OverlinedInfoContainer.cs
index b11e41f90f..9f56a34aa6 100644
--- a/osu.Game/Overlays/Profile/Header/Components/OverlinedInfoContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/OverlinedInfoContainer.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
Font = OsuFont.GetFont(size: big ? 40 : 18, weight: FontWeight.Light)
},
- new Container //Add a minimum size to the FillFlowContainer
+ new Container // Add a minimum size to the FillFlowContainer
{
Width = minimumWidth,
}
diff --git a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs
index a5938a3fe7..e7df4eb5eb 100644
--- a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Profile.Header
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
- new Container //artificial shadow
+ new Container // artificial shadow
{
RelativeSizeAxes = Axes.X,
Height = 3,
diff --git a/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs b/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs
index a33f4eb30d..5ecb477a2f 100644
--- a/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs
+++ b/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs
@@ -91,6 +91,8 @@ namespace osu.Game.Overlays.SearchableList
protected override void Dispose(bool isDisposing)
{
+ base.Dispose(isDisposing);
+
bindable.ValueChanged -= Bindable_ValueChanged;
}
}
diff --git a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs
index 117f905de4..d31470e685 100644
--- a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs
+++ b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs
@@ -94,7 +94,7 @@ namespace osu.Game.Overlays.SearchableList
RelativeSizeAxes = Axes.X,
},
},
- new Box //keep the tab strip part of autosize, but don't put it in the flow container
+ new Box // keep the tab strip part of autosize, but don't put it in the flow container
{
RelativeSizeAxes = Axes.X,
Height = 1,
diff --git a/osu.Game/Overlays/Settings/Sections/AudioSection.cs b/osu.Game/Overlays/Settings/Sections/AudioSection.cs
index b18488b616..69538358f1 100644
--- a/osu.Game/Overlays/Settings/Sections/AudioSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/AudioSection.cs
@@ -13,9 +13,12 @@ namespace osu.Game.Overlays.Settings.Sections
{
public override string Header => "Audio";
- public override IEnumerable FilterTerms => base.FilterTerms.Concat(new[] { "sound" });
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.VolumeUp
+ };
- public override IconUsage Icon => FontAwesome.Solid.VolumeUp;
+ public override IEnumerable FilterTerms => base.FilterTerms.Concat(new[] { "sound" });
public AudioSection()
{
diff --git a/osu.Game/Overlays/Settings/Sections/DebugSection.cs b/osu.Game/Overlays/Settings/Sections/DebugSection.cs
index f62de0b243..44d4088972 100644
--- a/osu.Game/Overlays/Settings/Sections/DebugSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/DebugSection.cs
@@ -10,7 +10,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class DebugSection : SettingsSection
{
public override string Header => "Debug";
- public override IconUsage Icon => FontAwesome.Solid.Bug;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Bug
+ };
public DebugSection()
{
diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs
index 97d9d3c697..aca507f20a 100644
--- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs
+++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs
@@ -13,7 +13,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class GameplaySection : SettingsSection
{
public override string Header => "Gameplay";
- public override IconUsage Icon => FontAwesome.Regular.Circle;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Regular.Circle
+ };
public GameplaySection()
{
diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs
index d9947f16cc..fefc3fe6a7 100644
--- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs
@@ -10,7 +10,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class GeneralSection : SettingsSection
{
public override string Header => "General";
- public override IconUsage Icon => FontAwesome.Solid.Cog;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Cog
+ };
public GeneralSection()
{
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index b73c8f7622..00b7643332 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -209,15 +209,16 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private IReadOnlyList getResolutions()
{
var resolutions = new List { new Size(9999, 9999) };
+ var currentDisplay = game.Window?.CurrentDisplay.Value;
- if (game.Window != null)
+ if (currentDisplay != null)
{
- resolutions.AddRange(game.Window.AvailableResolutions
- .Where(r => r.Width >= 800 && r.Height >= 600)
- .OrderByDescending(r => r.Width)
- .ThenByDescending(r => r.Height)
- .Select(res => new Size(res.Width, res.Height))
- .Distinct());
+ resolutions.AddRange(currentDisplay.DisplayModes
+ .Where(m => m.Size.Width >= 800 && m.Size.Height >= 600)
+ .OrderByDescending(m => m.Size.Width)
+ .ThenByDescending(m => m.Size.Height)
+ .Select(m => m.Size)
+ .Distinct());
}
return resolutions;
diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs
index 89caa3dc8f..c1b4b0bbcb 100644
--- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs
@@ -10,7 +10,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class GraphicsSection : SettingsSection
{
public override string Header => "Graphics";
- public override IconUsage Icon => FontAwesome.Solid.Laptop;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Laptop
+ };
public GraphicsSection()
{
diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs
index 2a348b4e03..b43453f53d 100644
--- a/osu.Game/Overlays/Settings/Sections/InputSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs
@@ -10,7 +10,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class InputSection : SettingsSection
{
public override string Header => "Input";
- public override IconUsage Icon => FontAwesome.Regular.Keyboard;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Keyboard
+ };
public InputSection(KeyBindingPanel keyConfig)
{
diff --git a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs
index 0f3acd5b7f..73c88b8e71 100644
--- a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs
@@ -11,7 +11,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class MaintenanceSection : SettingsSection
{
public override string Header => "Maintenance";
- public override IconUsage Icon => FontAwesome.Solid.Wrench;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Wrench
+ };
public MaintenanceSection()
{
diff --git a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs
index 80295690c0..150cddb388 100644
--- a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs
@@ -10,7 +10,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class OnlineSection : SettingsSection
{
public override string Header => "Online";
- public override IconUsage Icon => FontAwesome.Solid.GlobeAsia;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.GlobeAsia
+ };
public OnlineSection()
{
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index b229014c84..75c8db1612 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -19,7 +19,10 @@ namespace osu.Game.Overlays.Settings.Sections
public override string Header => "Skin";
- public override IconUsage Icon => FontAwesome.Solid.PaintBrush;
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.PaintBrush
+ };
private readonly Bindable dropdownBindable = new Bindable { Default = SkinInfo.Default };
private readonly Bindable configBindable = new Bindable();
diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs
index be3696029e..97e4ba9da7 100644
--- a/osu.Game/Overlays/Settings/SettingsSection.cs
+++ b/osu.Game/Overlays/Settings/SettingsSection.cs
@@ -11,7 +11,6 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Graphics.Sprites;
namespace osu.Game.Overlays.Settings
{
@@ -20,7 +19,7 @@ namespace osu.Game.Overlays.Settings
protected FillFlowContainer FlowContent;
protected override Container Content => FlowContent;
- public abstract IconUsage Icon { get; }
+ public abstract Drawable CreateIcon();
public abstract string Header { get; }
public IEnumerable FilterableChildren => Children.OfType();
diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs
index 68836bc6b3..30a53b351d 100644
--- a/osu.Game/Overlays/Settings/SidebarButton.cs
+++ b/osu.Game/Overlays/Settings/SidebarButton.cs
@@ -11,12 +11,13 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Settings
{
public class SidebarButton : OsuButton
{
- private readonly SpriteIcon drawableIcon;
+ private readonly ConstrainedIconContainer iconContainer;
private readonly SpriteText headerText;
private readonly Box selectionIndicator;
private readonly Container text;
@@ -30,7 +31,7 @@ namespace osu.Game.Overlays.Settings
{
section = value;
headerText.Text = value.Header;
- drawableIcon.Icon = value.Icon;
+ iconContainer.Icon = value.CreateIcon();
}
}
@@ -78,7 +79,7 @@ namespace osu.Game.Overlays.Settings
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
- drawableIcon = new SpriteIcon
+ iconContainer = new ConstrainedIconContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index 227347112c..1b748cb672 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -33,6 +33,9 @@ namespace osu.Game.Overlays.Toolbar
private readonly Bindable overlayActivationMode = new Bindable(OverlayActivation.All);
+ // Toolbar components like RulesetSelector should receive keyboard input events even when the toolbar is hidden.
+ public override bool PropagateNonPositionalInputSubTree => true;
+
public Toolbar()
{
RelativeSizeAxes = Axes.X;
@@ -148,7 +151,7 @@ namespace osu.Game.Overlays.Toolbar
protected override void PopOut()
{
- userButton?.StateContainer.Hide();
+ userButton.StateContainer?.Hide();
this.MoveToY(-DrawSize.Y, transition_time, Easing.OutQuint);
this.FadeOut(transition_time);
diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
index d6b810366d..3d66d3c28e 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
@@ -110,7 +110,7 @@ namespace osu.Game.Overlays.Toolbar
tooltipContainer = new FillFlowContainer
{
Direction = FillDirection.Vertical,
- RelativeSizeAxes = Axes.Both, //stops us being considered in parent's autosize
+ RelativeSizeAxes = Axes.Both, // stops us being considered in parent's autosize
Anchor = TooltipAnchor.HasFlag(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight,
Origin = TooltipAnchor,
Position = new Vector2(TooltipAnchor.HasFlag(Anchor.x0) ? 5 : -5, 5),
diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs
index b484921cce..676d2c941a 100644
--- a/osu.Game/Overlays/VolumeOverlay.cs
+++ b/osu.Game/Overlays/VolumeOverlay.cs
@@ -58,7 +58,7 @@ namespace osu.Game.Overlays
{
volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker)
{
- Margin = new MarginPadding { Top = 100 + MuteButton.HEIGHT } //to counter the mute button and re-center the volume meters
+ Margin = new MarginPadding { Top = 100 + MuteButton.HEIGHT } // to counter the mute button and re-center the volume meters
},
volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker),
volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker),
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 0047142cbd..ba6571fe1a 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
[Cached(typeof(DrawableHitObject))]
public abstract class DrawableHitObject : SkinReloadableDrawable
{
+ public event Action DefaultsApplied;
+
public readonly HitObject HitObject;
///
@@ -148,7 +150,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
samplesBindable.CollectionChanged += (_, __) => loadSamples();
updateState(ArmedState.Idle, true);
- onDefaultsApplied();
+ apply(HitObject);
}
private void loadSamples()
@@ -175,7 +177,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
AddInternal(Samples);
}
- private void onDefaultsApplied() => apply(HitObject);
+ private void onDefaultsApplied(HitObject hitObject)
+ {
+ apply(hitObject);
+ DefaultsApplied?.Invoke(this);
+ }
private void apply(HitObject hitObject)
{
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index 9a8efdde84..cffbdbae08 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Objects
///
/// Invoked after has completed on this .
///
- public event Action DefaultsApplied;
+ public event Action DefaultsApplied;
public readonly Bindable StartTimeBindable = new BindableDouble();
@@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Objects
foreach (var h in nestedHitObjects)
h.ApplyDefaults(controlPointInfo, difficulty);
- DefaultsApplied?.Invoke();
+ DefaultsApplied?.Invoke(this);
}
protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
index 7e17396fde..55d82c4083 100644
--- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
+++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Replays
{
int newFrame = nextFrameIndex;
- //ensure we aren't at an extent.
+ // ensure we aren't at an extent.
if (newFrame == currentFrameIndex) return false;
currentFrameIndex = newFrame;
@@ -99,8 +99,8 @@ namespace osu.Game.Rulesets.Replays
if (frame == null)
return false;
- return IsImportant(frame) && //a button is in a pressed state
- Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= AllowedImportantTimeSpan; //the next frame is within an allowable time span
+ return IsImportant(frame) && // a button is in a pressed state
+ Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= AllowedImportantTimeSpan; // the next frame is within an allowable time span
}
}
diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs
index afd499cb9e..2e32b96084 100644
--- a/osu.Game/Rulesets/RulesetInfo.cs
+++ b/osu.Game/Rulesets/RulesetInfo.cs
@@ -3,6 +3,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using Newtonsoft.Json;
namespace osu.Game.Rulesets
@@ -15,7 +16,20 @@ namespace osu.Game.Rulesets
public string ShortName { get; set; }
- public string InstantiationInfo { get; set; }
+ private string instantiationInfo;
+
+ public string InstantiationInfo
+ {
+ get => instantiationInfo;
+ set => instantiationInfo = abbreviateInstantiationInfo(value);
+ }
+
+ private string abbreviateInstantiationInfo(string value)
+ {
+ // exclude version onwards, matching only on namespace and type.
+ // this is mainly to allow for new versions of already loaded rulesets to "upgrade" from old.
+ return string.Join(',', value.Split(',').Take(2));
+ }
[JsonIgnore]
public bool Available { get; set; }
diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index 543134cfb4..b3026bf2b7 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Rulesets
var instances = loadedAssemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r)).ToList();
- //add all legacy rulesets first to ensure they have exclusive choice of primary key.
+ // add all legacy rulesets first to ensure they have exclusive choice of primary key.
foreach (var r in instances.Where(r => r is ILegacyRuleset))
{
if (context.RulesetInfo.SingleOrDefault(dbRuleset => dbRuleset.ID == r.RulesetInfo.ID) == null)
@@ -90,27 +90,23 @@ namespace osu.Game.Rulesets
context.SaveChanges();
- //add any other modes
+ // add any other modes
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
{
- if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null)
+ // todo: StartsWith can be changed to Equals on 2020-11-08
+ // This is to give users enough time to have their database use new abbreviated info).
+ if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo)) == null)
context.RulesetInfo.Add(r.RulesetInfo);
}
context.SaveChanges();
- //perform a consistency check
+ // perform a consistency check
foreach (var r in context.RulesetInfo)
{
try
{
- var instanceInfo = ((Ruleset)Activator.CreateInstance(Type.GetType(r.InstantiationInfo, asm =>
- {
- // for the time being, let's ignore the version being loaded.
- // this allows for debug builds to successfully load rulesets (even though debug rulesets have a 0.0.0 version).
- asm.Version = null;
- return Assembly.Load(asm);
- }, null))).RulesetInfo;
+ var instanceInfo = ((Ruleset)Activator.CreateInstance(Type.GetType(r.InstantiationInfo))).RulesetInfo;
r.Name = instanceInfo.Name;
r.ShortName = instanceInfo.ShortName;
diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs
index 7ba88d3df8..b057af2a50 100644
--- a/osu.Game/Rulesets/Scoring/HitResult.cs
+++ b/osu.Game/Rulesets/Scoring/HitResult.cs
@@ -43,5 +43,25 @@ namespace osu.Game.Rulesets.Scoring
///
[Description(@"Perfect")]
Perfect,
+
+ ///
+ /// Indicates small tick miss.
+ ///
+ SmallTickMiss,
+
+ ///
+ /// Indicates a small tick hit.
+ ///
+ SmallTickHit,
+
+ ///
+ /// Indicates a large tick miss.
+ ///
+ LargeTickMiss,
+
+ ///
+ /// Indicates a large tick hit.
+ ///
+ LargeTickHit
}
}
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index 5062c92afe..9a10b7d1b2 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.UI
///
public PassThroughInputManager KeyBindingInputManager;
- public override double GameplayStartTime => Objects.First().StartTime - 2000;
+ public override double GameplayStartTime => Objects.FirstOrDefault()?.StartTime - 2000 ?? 0;
private readonly Lazy playfield;
@@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.UI
/// The mods which are to be applied.
///
[Cached(typeof(IReadOnlyList))]
- private readonly IReadOnlyList mods;
+ protected readonly IReadOnlyList Mods;
private FrameStabilityContainer frameStabilityContainer;
@@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.UI
throw new ArgumentException($"{GetType()} expected the beatmap to contain hitobjects of type {typeof(TObject)}.", nameof(beatmap));
Beatmap = tBeatmap;
- this.mods = mods?.ToArray() ?? Array.Empty();
+ Mods = mods?.ToArray() ?? Array.Empty();
RelativeSizeAxes = Axes.Both;
@@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.UI
.WithChild(ResumeOverlay)));
}
- applyRulesetMods(mods, config);
+ applyRulesetMods(Mods, config);
loadObjects(cancellationToken);
}
@@ -224,7 +224,7 @@ namespace osu.Game.Rulesets.UI
Playfield.PostProcess();
- foreach (var mod in mods.OfType())
+ foreach (var mod in Mods.OfType())
mod.ApplyToDrawableHitObjects(Playfield.AllHitObjects);
}
@@ -487,6 +487,11 @@ namespace osu.Game.Rulesets.UI
protected virtual ResumeOverlay CreateResumeOverlay() => null;
+ ///
+ /// Whether to display gameplay overlays, such as and .
+ ///
+ public virtual bool AllowGameplayOverlays => true;
+
///
/// Sets a replay to be used, overriding local input.
///
diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs
index dea981c3ad..f4f66f1272 100644
--- a/osu.Game/Rulesets/UI/HitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs
@@ -43,6 +43,15 @@ namespace osu.Game.Rulesets.UI
return true;
}
+ public virtual void Clear(bool disposeChildren = true)
+ {
+ ClearInternal(disposeChildren);
+
+ foreach (var kvp in startTimeMap)
+ kvp.Value.bindable.UnbindAll();
+ startTimeMap.Clear();
+ }
+
public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject);
private void onStartTimeChanged(DrawableHitObject hitObject)
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 108f98d5fc..15e625872d 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -16,17 +16,23 @@ namespace osu.Game.Rulesets.UI.Scrolling
{
private readonly IBindable timeRange = new BindableDouble();
private readonly IBindable direction = new Bindable();
+ private readonly Dictionary hitObjectInitialStateCache = new Dictionary();
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
- private readonly LayoutValue initialStateCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
+ // Responds to changes in the layout. When the layout changes, all hit object states must be recomputed.
+ private readonly LayoutValue layoutCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
+
+ // A combined cache across all hit object states to reduce per-update iterations.
+ // When invalidated, one or more (but not necessarily all) hitobject states must be re-validated.
+ private readonly Cached combinedObjCache = new Cached();
public ScrollingHitObjectContainer()
{
RelativeSizeAxes = Axes.Both;
- AddLayout(initialStateCache);
+ AddLayout(layoutCache);
}
[BackgroundDependencyLoader]
@@ -35,13 +41,14 @@ namespace osu.Game.Rulesets.UI.Scrolling
direction.BindTo(scrollingInfo.Direction);
timeRange.BindTo(scrollingInfo.TimeRange);
- direction.ValueChanged += _ => initialStateCache.Invalidate();
- timeRange.ValueChanged += _ => initialStateCache.Invalidate();
+ direction.ValueChanged += _ => layoutCache.Invalidate();
+ timeRange.ValueChanged += _ => layoutCache.Invalidate();
}
public override void Add(DrawableHitObject hitObject)
{
- initialStateCache.Invalidate();
+ combinedObjCache.Invalidate();
+ hitObject.DefaultsApplied += onDefaultsApplied;
base.Add(hitObject);
}
@@ -51,24 +58,56 @@ namespace osu.Game.Rulesets.UI.Scrolling
if (result)
{
- initialStateCache.Invalidate();
+ combinedObjCache.Invalidate();
hitObjectInitialStateCache.Remove(hitObject);
+
+ hitObject.DefaultsApplied -= onDefaultsApplied;
}
return result;
}
+ public override void Clear(bool disposeChildren = true)
+ {
+ foreach (var h in Objects)
+ h.DefaultsApplied -= onDefaultsApplied;
+
+ base.Clear(disposeChildren);
+
+ combinedObjCache.Invalidate();
+ hitObjectInitialStateCache.Clear();
+ }
+
+ private void onDefaultsApplied(DrawableHitObject drawableObject)
+ {
+ // The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame).
+ // In such a case, combinedObjCache will take care of updating the hitobject.
+ if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var objCache))
+ {
+ combinedObjCache.Invalidate();
+ objCache.Invalidate();
+ }
+ }
+
private float scrollLength;
protected override void Update()
{
base.Update();
- if (!initialStateCache.IsValid)
+ if (!layoutCache.IsValid)
{
foreach (var cached in hitObjectInitialStateCache.Values)
cached.Invalidate();
+ combinedObjCache.Invalidate();
+ scrollingInfo.Algorithm.Reset();
+
+ layoutCache.Validate();
+ }
+
+ if (!combinedObjCache.IsValid)
+ {
switch (direction.Value)
{
case ScrollingDirection.Up:
@@ -81,15 +120,21 @@ namespace osu.Game.Rulesets.UI.Scrolling
break;
}
- scrollingInfo.Algorithm.Reset();
-
foreach (var obj in Objects)
{
+ if (!hitObjectInitialStateCache.TryGetValue(obj, out var objCache))
+ objCache = hitObjectInitialStateCache[obj] = new Cached();
+
+ if (objCache.IsValid)
+ continue;
+
computeLifetimeStartRecursive(obj);
computeInitialStateRecursive(obj);
+
+ objCache.Validate();
}
- initialStateCache.Validate();
+ combinedObjCache.Validate();
}
}
@@ -101,8 +146,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
computeLifetimeStartRecursive(obj);
}
- private readonly Dictionary hitObjectInitialStateCache = new Dictionary();
-
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
{
float originAdjustment = 0.0f;
@@ -134,12 +177,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Cant use AddOnce() since the delegate is re-constructed every invocation
private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
{
- if (!hitObjectInitialStateCache.TryGetValue(hitObject, out var cached))
- cached = hitObjectInitialStateCache[hitObject] = new Cached();
-
- if (cached.IsValid)
- return;
-
if (hitObject.HitObject is IHasEndTime e)
{
switch (direction.Value)
@@ -163,8 +200,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Nested hitobjects don't need to scroll, but they do need accurate positions
updatePosition(obj, hitObject.HitObject.StartTime);
}
-
- cached.Validate();
});
protected override void UpdateAfterChildrenLife()
diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
index 66b1acf591..6f73a284a2 100644
--- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
+++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Scoring.Legacy
switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID)
{
case 3:
- return scoreInfo.Statistics[HitResult.Perfect];
+ return getCount(scoreInfo, HitResult.Perfect);
}
return null;
@@ -35,10 +35,10 @@ namespace osu.Game.Scoring.Legacy
case 0:
case 1:
case 3:
- return scoreInfo.Statistics[HitResult.Great];
+ return getCount(scoreInfo, HitResult.Great);
case 2:
- return scoreInfo.Statistics[HitResult.Perfect];
+ return getCount(scoreInfo, HitResult.Perfect);
}
return null;
@@ -65,7 +65,10 @@ namespace osu.Game.Scoring.Legacy
switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID)
{
case 3:
- return scoreInfo.Statistics[HitResult.Good];
+ return getCount(scoreInfo, HitResult.Good);
+
+ case 2:
+ return getCount(scoreInfo, HitResult.SmallTickMiss);
}
return null;
@@ -78,6 +81,10 @@ namespace osu.Game.Scoring.Legacy
case 3:
scoreInfo.Statistics[HitResult.Good] = value;
break;
+
+ case 2:
+ scoreInfo.Statistics[HitResult.SmallTickMiss] = value;
+ break;
}
}
@@ -87,10 +94,13 @@ namespace osu.Game.Scoring.Legacy
{
case 0:
case 1:
- return scoreInfo.Statistics[HitResult.Good];
+ return getCount(scoreInfo, HitResult.Good);
case 3:
- return scoreInfo.Statistics[HitResult.Ok];
+ return getCount(scoreInfo, HitResult.Ok);
+
+ case 2:
+ return getCount(scoreInfo, HitResult.LargeTickHit);
}
return null;
@@ -108,6 +118,10 @@ namespace osu.Game.Scoring.Legacy
case 3:
scoreInfo.Statistics[HitResult.Ok] = value;
break;
+
+ case 2:
+ scoreInfo.Statistics[HitResult.LargeTickHit] = value;
+ break;
}
}
@@ -117,7 +131,10 @@ namespace osu.Game.Scoring.Legacy
{
case 0:
case 3:
- return scoreInfo.Statistics[HitResult.Meh];
+ return getCount(scoreInfo, HitResult.Meh);
+
+ case 2:
+ return getCount(scoreInfo, HitResult.SmallTickHit);
}
return null;
@@ -131,13 +148,25 @@ namespace osu.Game.Scoring.Legacy
case 3:
scoreInfo.Statistics[HitResult.Meh] = value;
break;
+
+ case 2:
+ scoreInfo.Statistics[HitResult.SmallTickHit] = value;
+ break;
}
}
public static int? GetCountMiss(this ScoreInfo scoreInfo) =>
- scoreInfo.Statistics[HitResult.Miss];
+ getCount(scoreInfo, HitResult.Miss);
public static void SetCountMiss(this ScoreInfo scoreInfo, int value) =>
scoreInfo.Statistics[HitResult.Miss] = value;
+
+ private static int? getCount(ScoreInfo scoreInfo, HitResult result)
+ {
+ if (scoreInfo.Statistics.TryGetValue(result, out var existing))
+ return existing;
+
+ return null;
+ }
}
}
diff --git a/osu.Game/Screens/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs
index 5dfaceccf5..0f3615b7a9 100644
--- a/osu.Game/Screens/BackgroundScreen.cs
+++ b/osu.Game/Screens/BackgroundScreen.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Screens
protected override bool OnKeyDown(KeyDownEvent e)
{
- //we don't want to handle escape key.
+ // we don't want to handle escape key.
return false;
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index ad16e22e5e..8910684463 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -401,12 +401,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
HitObject draggedObject = movementBlueprint.HitObject;
- // The final movement position, relative to screenSpaceMovementStartPosition
+ // The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
+ // Retrieve a snapped position.
(Vector2 snappedPosition, double snappedTime) = snapProvider.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime);
- // Move the hitobjects
+ // Move the hitobjects.
if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, ToScreenSpace(snappedPosition))))
return true;
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs
index 0d4c48b5f6..8c8b38d9ea 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs
@@ -12,14 +12,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class CentreMarker : CompositeDrawable
{
- private const float triangle_width = 20;
+ private const float triangle_width = 15;
private const float triangle_height = 10;
private const float bar_width = 2;
public CentreMarker()
{
RelativeSizeAxes = Axes.Y;
- Size = new Vector2(20, 1);
+ Size = new Vector2(triangle_width, 1);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@@ -39,6 +39,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Origin = Anchor.BottomCentre,
Size = new Vector2(triangle_width, triangle_height),
Scale = new Vector2(1, -1)
+ },
+ new Triangle
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Size = new Vector2(triangle_width, triangle_height),
}
};
}
@@ -46,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- Colour = colours.Red;
+ Colour = colours.RedDark;
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index 1cb4f737c1..25f3cfc285 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
});
// We don't want the centre marker to scroll
- AddInternal(new CentreMarker());
+ AddInternal(new CentreMarker { Depth = float.MaxValue });
WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
@@ -60,9 +60,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
waveform.Waveform = b.NewValue.Waveform;
track = b.NewValue.Track;
- MaxZoom = getZoomLevelForVisibleMilliseconds(500);
- MinZoom = getZoomLevelForVisibleMilliseconds(10000);
- Zoom = getZoomLevelForVisibleMilliseconds(2000);
+ if (track.Length > 0)
+ {
+ MaxZoom = getZoomLevelForVisibleMilliseconds(500);
+ MinZoom = getZoomLevelForVisibleMilliseconds(10000);
+ Zoom = getZoomLevelForVisibleMilliseconds(2000);
+ }
}, true);
}
@@ -135,7 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void scrollToTrackTime()
{
- if (!track.IsLoaded)
+ if (!track.IsLoaded || track.Length == 0)
return;
ScrollTo((float)(adjustableClock.CurrentTime / track.Length) * Content.DrawWidth, false);
diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs
index a2d2f08ce9..23c8c9f605 100644
--- a/osu.Game/Screens/Edit/EditorBeatmap.cs
+++ b/osu.Game/Screens/Edit/EditorBeatmap.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -136,14 +137,26 @@ namespace osu.Game.Screens.Edit
/// The to add.
public void Add(HitObject hitObject)
{
- trackStartTime(hitObject);
-
// Preserve existing sorting order in the beatmap
var insertionIndex = findInsertionIndex(PlayableBeatmap.HitObjects, hitObject.StartTime);
- mutableHitObjects.Insert(insertionIndex + 1, hitObject);
+ Insert(insertionIndex + 1, hitObject);
+ }
+
+ ///
+ /// Inserts a into this .
+ ///
+ ///
+ /// It is the invoker's responsibility to make sure that sorting order is maintained.
+ ///
+ /// The index to insert the at.
+ /// The to insert.
+ public void Insert(int index, HitObject hitObject)
+ {
+ trackStartTime(hitObject);
+
+ mutableHitObjects.Insert(index, hitObject);
HitObjectAdded?.Invoke(hitObject);
-
updateHitObject(hitObject, true);
}
@@ -189,6 +202,25 @@ namespace osu.Game.Screens.Edit
updateHitObject(null, true);
}
+ ///
+ /// Clears all from this .
+ ///
+ public void Clear()
+ {
+ var removed = HitObjects.ToList();
+
+ mutableHitObjects.Clear();
+
+ foreach (var b in startTimeBindables)
+ b.Value.UnbindAll();
+ startTimeBindables.Clear();
+
+ foreach (var h in removed)
+ HitObjectRemoved?.Invoke(h);
+
+ updateHitObject(null, true);
+ }
+
private void trackStartTime(HitObject hitObject)
{
startTimeBindables[hitObject] = hitObject.StartTimeBindable.GetBoundCopy();
diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs
index 17eba87076..fc3dd4c105 100644
--- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs
+++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs
@@ -63,8 +63,10 @@ namespace osu.Game.Screens.Edit
}
}
- // Make the removal indices are sorted so that iteration order doesn't get messed up post-removal.
+ // Sort the indices to ensure that removal + insertion indices don't get jumbled up post-removal or post-insertion.
+ // This isn't strictly required, but the differ makes no guarantees about order.
toRemove.Sort();
+ toAdd.Sort();
// Apply the changes.
for (int i = toRemove.Count - 1; i >= 0; i--)
@@ -74,7 +76,7 @@ namespace osu.Game.Screens.Edit
{
IBeatmap newBeatmap = readBeatmap(newState);
foreach (var i in toAdd)
- editorBeatmap.Add(newBeatmap.HitObjects[i]);
+ editorBeatmap.Insert(i, newBeatmap.HitObjects[i]);
}
}
@@ -84,7 +86,11 @@ namespace osu.Game.Screens.Edit
{
using (var stream = new MemoryStream(state))
using (var reader = new LineBufferedReader(stream, true))
- return new PassThroughWorkingBeatmap(Decoder.GetDecoder(reader).Decode(reader)).GetPlayableBeatmap(editorBeatmap.BeatmapInfo.Ruleset);
+ {
+ var decoded = Decoder.GetDecoder(reader).Decode(reader);
+ decoded.BeatmapInfo.Ruleset = editorBeatmap.BeatmapInfo.Ruleset;
+ return new PassThroughWorkingBeatmap(decoded).GetPlayableBeatmap(editorBeatmap.BeatmapInfo.Ruleset);
+ }
}
private class PassThroughWorkingBeatmap : WorkingBeatmap
diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs
index 26455b1dbd..0d5f3d1142 100644
--- a/osu.Game/Screens/Menu/IntroScreen.cs
+++ b/osu.Game/Screens/Menu/IntroScreen.cs
@@ -73,7 +73,7 @@ namespace osu.Game.Screens.Menu
if (!MenuMusic.Value)
{
- var sets = beatmaps.GetAllUsableBeatmapSets();
+ var sets = beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal);
if (sets.Count > 0)
setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID);
}
@@ -96,14 +96,12 @@ namespace osu.Game.Screens.Menu
Track = introBeatmap.Track;
}
- public override bool OnExiting(IScreen next) => !DidLoadMenu;
-
public override void OnResuming(IScreen last)
{
this.FadeIn(300);
double fadeOutTime = exit_delay;
- //we also handle the exit transition.
+ // we also handle the exit transition.
if (MenuVoice.Value)
seeya.Play();
else
diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs
index 67537fa9df..0db7f2a2dc 100644
--- a/osu.Game/Screens/Menu/LogoVisualisation.cs
+++ b/osu.Game/Screens/Menu/LogoVisualisation.cs
@@ -162,7 +162,7 @@ namespace osu.Game.Screens.Menu
private IShader shader;
private Texture texture;
- //Assuming the logo is a circle, we don't need a second dimension.
+ // Assuming the logo is a circle, we don't need a second dimension.
private float size;
private Color4 colour;
@@ -209,13 +209,13 @@ namespace osu.Game.Screens.Menu
float rotation = MathUtils.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds);
float rotationCos = MathF.Cos(rotation);
float rotationSin = MathF.Sin(rotation);
- //taking the cos and sin to the 0..1 range
+ // taking the cos and sin to the 0..1 range
var barPosition = new Vector2(rotationCos / 2 + 0.5f, rotationSin / 2 + 0.5f) * size;
var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(MathUtils.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]);
- //The distance between the position and the sides of the bar.
+ // The distance between the position and the sides of the bar.
var bottomOffset = new Vector2(-rotationSin * barSize.X / 2, rotationCos * barSize.X / 2);
- //The distance between the bottom side of the bar and the top side.
+ // The distance between the bottom side of the bar and the top side.
var amplitudeOffset = new Vector2(rotationCos * barSize.Y, rotationSin * barSize.Y);
var rectangle = new Quad(
@@ -231,7 +231,7 @@ namespace osu.Game.Screens.Menu
colourInfo,
null,
vertexBatch.AddAction,
- //barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that.
+ // barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that.
Vector2.Divide(inflation, barSize.Yx));
}
}
diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs
index 0589e4d12b..f0da2482d6 100644
--- a/osu.Game/Screens/Menu/MainMenu.cs
+++ b/osu.Game/Screens/Menu/MainMenu.cs
@@ -250,7 +250,7 @@ namespace osu.Game.Screens.Menu
(Background as BackgroundScreenDefault)?.Next();
- //we may have consumed our preloaded instance, so let's make another.
+ // we may have consumed our preloaded instance, so let's make another.
preloadSongSelect();
if (Beatmap.Value.Track != null && music?.IsUserPaused != true)
diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs
index 61e94ae969..35bb4fa34f 100644
--- a/osu.Game/Screens/OsuScreen.cs
+++ b/osu.Game/Screens/OsuScreen.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
-using Microsoft.EntityFrameworkCore.Internal;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -24,14 +23,14 @@ namespace osu.Game.Screens
{
///
/// The amount of negative padding that should be applied to game background content which touches both the left and right sides of the screen.
- /// This allows for the game content to be pushed byt he options/notification overlays without causing black areas to appear.
+ /// This allows for the game content to be pushed by the options/notification overlays without causing black areas to appear.
///
public const float HORIZONTAL_OVERFLOW_PADDING = 50;
///
/// A user-facing title for this screen.
///
- public virtual string Title => GetType().ShortDisplayName();
+ public virtual string Title => GetType().Name;
public string Description => Title;
diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs
index c978f4e96d..36f825b8f6 100644
--- a/osu.Game/Screens/Play/BreakOverlay.cs
+++ b/osu.Game/Screens/Play/BreakOverlay.cs
@@ -119,7 +119,7 @@ namespace osu.Game.Screens.Play
FinishTransforms(true);
Scheduler.CancelDelayedTasks();
- if (breaks == null) return; //we need breaks.
+ if (breaks == null) return; // we need breaks.
foreach (var b in breaks)
{
diff --git a/osu.Game/Screens/Play/BreakTracker.cs b/osu.Game/Screens/Play/BreakTracker.cs
index fcd7ed6b73..51e21656e1 100644
--- a/osu.Game/Screens/Play/BreakTracker.cs
+++ b/osu.Game/Screens/Play/BreakTracker.cs
@@ -2,40 +2,37 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Utils;
namespace osu.Game.Screens.Play
{
public class BreakTracker : Component
{
private readonly ScoreProcessor scoreProcessor;
-
private readonly double gameplayStartTime;
+ private PeriodTracker breaks;
+
///
/// Whether the gameplay is currently in a break.
///
public IBindable IsBreakTime => isBreakTime;
- protected int CurrentBreakIndex;
-
private readonly BindableBool isBreakTime = new BindableBool();
- private IReadOnlyList breaks;
-
public IReadOnlyList Breaks
{
- get => breaks;
set
{
- breaks = value;
-
- // reset index in case the new breaks list is smaller than last one
isBreakTime.Value = false;
- CurrentBreakIndex = 0;
+
+ breaks = new PeriodTracker(value.Where(b => b.HasEffect)
+ .Select(b => new Period(b.StartTime, b.EndTime - BreakOverlay.BREAK_FADE_DURATION)));
}
}
@@ -49,34 +46,11 @@ namespace osu.Game.Screens.Play
{
base.Update();
- isBreakTime.Value = getCurrentBreak()?.HasEffect == true
- || Clock.CurrentTime < gameplayStartTime
+ var time = Clock.CurrentTime;
+
+ isBreakTime.Value = breaks?.IsInAny(time) == true
+ || time < gameplayStartTime
|| scoreProcessor?.HasCompleted.Value == true;
}
-
- private BreakPeriod getCurrentBreak()
- {
- if (breaks?.Count > 0)
- {
- var time = Clock.CurrentTime;
-
- if (time > breaks[CurrentBreakIndex].EndTime)
- {
- while (time > breaks[CurrentBreakIndex].EndTime && CurrentBreakIndex < breaks.Count - 1)
- CurrentBreakIndex++;
- }
- else
- {
- while (time < breaks[CurrentBreakIndex].StartTime && CurrentBreakIndex > 0)
- CurrentBreakIndex--;
- }
-
- var closest = breaks[CurrentBreakIndex];
-
- return closest.Contains(time) ? closest : null;
- }
-
- return null;
- }
}
}
diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
index d201b5d30e..fc80983834 100644
--- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
+++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Screens.Play.HUD
protected override void PopIn() => this.FadeIn(fade_duration);
protected override void PopOut() => this.FadeOut(fade_duration);
- //We want to handle keyboard inputs all the time in order to trigger ToggleVisibility() when not visible
+ // We want to handle keyboard inputs all the time in order to trigger ToggleVisibility() when not visible
public override bool PropagateNonPositionalInputSubTree => true;
protected override bool OnKeyDown(KeyDownEvent e)
diff --git a/osu.Game/Screens/Play/KeyCounter.cs b/osu.Game/Screens/Play/KeyCounter.cs
index f4109a63d0..98df73a5e6 100644
--- a/osu.Game/Screens/Play/KeyCounter.cs
+++ b/osu.Game/Screens/Play/KeyCounter.cs
@@ -124,8 +124,8 @@ namespace osu.Game.Screens.Play
}
}
};
- //Set this manually because an element with Alpha=0 won't take it size to AutoSizeContainer,
- //so the size can be changing between buttonSprite and glowSprite.
+ // Set this manually because an element with Alpha=0 won't take it size to AutoSizeContainer,
+ // so the size can be changing between buttonSprite and glowSprite.
Height = buttonSprite.DrawHeight;
Width = buttonSprite.DrawWidth;
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index ece4c6307e..acc8dc9c7c 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -184,6 +184,13 @@ namespace osu.Game.Screens.Play
addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap);
addOverlayComponents(GameplayClockContainer, Beatmap.Value);
+ if (!DrawableRuleset.AllowGameplayOverlays)
+ {
+ HUDOverlay.ShowHud.Value = false;
+ HUDOverlay.ShowHud.Disabled = true;
+ BreakOverlay.Hide();
+ }
+
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
// bind clock into components that require it
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index c0d88feda2..93a734589c 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -314,8 +314,8 @@ namespace osu.Game.Screens.Play
LoadTask = null;
- //By default, we want to load the player and never be returned to.
- //Note that this may change if the player we load requested a re-run.
+ // By default, we want to load the player and never be returned to.
+ // Note that this may change if the player we load requested a re-run.
ValidForResume = false;
if (player.LoadedBeatmapSuccessfully)
@@ -360,7 +360,7 @@ namespace osu.Game.Screens.Play
{
if (!muteWarningShownOnce.Value)
{
- //Checks if the notification has not been shown yet and also if master volume is muted, track/music volume is muted or if the whole game is muted.
+ // Checks if the notification has not been shown yet and also if master volume is muted, track/music volume is muted or if the whole game is muted.
if (volumeOverlay?.IsMuted.Value == true || audioManager.Volume.Value <= audioManager.Volume.MinValue || audioManager.VolumeTrack.Value <= audioManager.VolumeTrack.MinValue)
{
notificationOverlay?.Post(new MutedNotification());
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index b058cc142b..fd8ac33aef 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -35,6 +35,8 @@ namespace osu.Game.Screens.Ranking.Expanded
private RollingCounter scoreCounter;
+ private const float padding = 10;
+
///
/// Creates a new .
///
@@ -46,7 +48,7 @@ namespace osu.Game.Screens.Ranking.Expanded
RelativeSizeAxes = Axes.Both;
Masking = true;
- Padding = new MarginPadding { Vertical = 10, Horizontal = 10 };
+ Padding = new MarginPadding(padding);
}
[BackgroundDependencyLoader]
@@ -92,13 +94,17 @@ namespace osu.Game.Screens.Ranking.Expanded
Origin = Anchor.TopCentre,
Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)),
Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
+ MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
+ Truncate = true,
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)),
- Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold)
+ Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
+ MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
+ Truncate = true,
},
new Container
{
diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs
index a1adfcc500..c055df7ccc 100644
--- a/osu.Game/Screens/Ranking/ScorePanel.cs
+++ b/osu.Game/Screens/Ranking/ScorePanel.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Screens.Ranking
///
/// Width of the panel when expanded.
///
- private const float expanded_width = 360;
+ public const float EXPANDED_WIDTH = 360;
///
/// Height of the panel when expanded.
@@ -183,7 +183,7 @@ namespace osu.Game.Screens.Ranking
switch (state)
{
case PanelState.Expanded:
- this.ResizeTo(new Vector2(expanded_width, expanded_height), resize_duration, Easing.OutQuint);
+ this.ResizeTo(new Vector2(EXPANDED_WIDTH, expanded_height), resize_duration, Easing.OutQuint);
topLayerBackground.FadeColour(expanded_top_layer_colour, resize_duration, Easing.OutQuint);
middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint);
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index f989ab2787..96b779cd20 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -169,7 +169,7 @@ namespace osu.Game.Screens.Select
loadBeatmapSets(GetLoadableBeatmaps());
}
- protected virtual IEnumerable GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable();
+ protected virtual IEnumerable GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.AllButFiles);
public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
{
@@ -208,7 +208,7 @@ namespace osu.Game.Screens.Select
// without this, during a large beatmap import it is impossible to navigate the carousel.
applyActiveCriteria(false, alwaysResetScrollPosition: false);
- //check if we can/need to maintain our current selection.
+ // check if we can/need to maintain our current selection.
if (previouslySelectedID != null)
select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == previouslySelectedID) ?? newSet);
diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs
index f4bf1ab059..63711e3e50 100644
--- a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs
+++ b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Screens.Select
set => tabs.Current = value;
}
- public Action OnFilter; //passed the selected tab and if mods is checked
+ public Action OnFilter; // passed the selected tab and if mods is checked
public IReadOnlyList TabItems
{
diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs
index aebb8e9d87..9669a1391c 100644
--- a/osu.Game/Screens/Select/BeatmapDetails.cs
+++ b/osu.Game/Screens/Select/BeatmapDetails.cs
@@ -201,7 +201,7 @@ namespace osu.Game.Screens.Select
Schedule(() =>
{
if (beatmap != requestedBeatmap)
- //the beatmap has been changed since we started the lookup.
+ // the beatmap has been changed since we started the lookup.
return;
var b = res.ToBeatmap(rulesets);
@@ -222,7 +222,7 @@ namespace osu.Game.Screens.Select
Schedule(() =>
{
if (beatmap != requestedBeatmap)
- //the beatmap has been changed since we started the lookup.
+ // the beatmap has been changed since we started the lookup.
return;
updateMetrics();
diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
index 6d760df065..ed54c158db 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter;
@@ -55,10 +54,7 @@ namespace osu.Game.Screens.Select.Carousel
if (match)
{
- var terms = new List();
-
- terms.AddRange(Beatmap.Metadata.SearchableTerms);
- terms.Add(Beatmap.Version);
+ var terms = Beatmap.SearchableTerms;
foreach (var criteriaTerm in criteria.SearchTerms)
match &= terms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0);
diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs
index af0d36ea9a..02822ea608 100644
--- a/osu.Game/Screens/Select/Details/AdvancedStats.cs
+++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Screens.Select.Details
AutoSizeAxes = Axes.Y,
Children = new[]
{
- FirstValue = new StatisticRow(), //circle size/key amount
+ FirstValue = new StatisticRow(), // circle size/key amount
HpDrain = new StatisticRow { Title = "HP Drain" },
Accuracy = new StatisticRow { Title = "Accuracy" },
ApproachRate = new StatisticRow { Title = "Approach Rate" },
diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs
index 179aab54a3..21ddc5685d 100644
--- a/osu.Game/Screens/Select/PlaySongSelect.cs
+++ b/osu.Game/Screens/Select/PlaySongSelect.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Screens;
using osu.Game.Graphics;
+using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Users;
@@ -32,9 +33,12 @@ namespace osu.Game.Screens.Select
Edit();
}, Key.Number4);
- ((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += score => this.Push(new ResultsScreen(score));
+ ((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += PresentScore;
}
+ protected void PresentScore(ScoreInfo score) =>
+ FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new ResultsScreen(score)));
+
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
public override void OnResuming(IScreen last)
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 0d07a335cf..d613b0ae8d 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -34,6 +34,7 @@ using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Scoring;
namespace osu.Game.Screens.Select
@@ -77,7 +78,7 @@ namespace osu.Game.Screens.Select
protected BeatmapCarousel Carousel { get; private set; }
- private DifficultyRecommender recommender;
+ private readonly DifficultyRecommender recommender = new DifficultyRecommender();
private BeatmapInfoWedge beatmapInfoWedge;
private DialogOverlay dialogOverlay;
@@ -92,6 +93,8 @@ namespace osu.Game.Screens.Select
private SampleChannel sampleChangeDifficulty;
private SampleChannel sampleChangeBeatmap;
+ private Container carouselContainer;
+
protected BeatmapDetailArea BeatmapDetails { get; private set; }
private readonly Bindable decoupledRuleset = new Bindable();
@@ -105,9 +108,22 @@ namespace osu.Game.Screens.Select
// initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter).
transferRulesetValue();
+ LoadComponentAsync(Carousel = new BeatmapCarousel
+ {
+ AllowSelection = false, // delay any selection until our bindables are ready to make a good choice.
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ RelativeSizeAxes = Axes.Both,
+ BleedTop = FilterControl.HEIGHT,
+ BleedBottom = Footer.HEIGHT,
+ SelectionChanged = updateSelectedBeatmap,
+ BeatmapSetsChanged = carouselBeatmapsLoaded,
+ GetRecommendedBeatmap = recommender.GetRecommendedBeatmap,
+ }, c => carouselContainer.Child = c);
+
AddRangeInternal(new Drawable[]
{
- recommender = new DifficultyRecommender(),
+ recommender,
new ResetScrollContainer(() => Carousel.ScrollToSelected())
{
RelativeSizeAxes = Axes.Y,
@@ -139,7 +155,7 @@ namespace osu.Game.Screens.Select
Padding = new MarginPadding { Right = -150 },
},
},
- new Container
+ carouselContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
@@ -147,18 +163,7 @@ namespace osu.Game.Screens.Select
Top = FilterControl.HEIGHT,
Bottom = Footer.HEIGHT
},
- Child = Carousel = new BeatmapCarousel
- {
- AllowSelection = false, // delay any selection until our bindables are ready to make a good choice.
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- RelativeSizeAxes = Axes.Both,
- BleedTop = FilterControl.HEIGHT,
- BleedBottom = Footer.HEIGHT,
- SelectionChanged = updateSelectedBeatmap,
- BeatmapSetsChanged = carouselBeatmapsLoaded,
- GetRecommendedBeatmap = recommender.GetRecommendedBeatmap,
- },
+ Child = new LoadingSpinner(true) { State = { Value = Visibility.Visible } }
}
},
}
@@ -286,7 +291,7 @@ namespace osu.Game.Screens.Select
Schedule(() =>
{
// if we have no beatmaps but osu-stable is found, let's prompt the user to import.
- if (!beatmaps.GetAllUsableBeatmapSetsEnumerable().Any() && beatmaps.StableInstallationAvailable)
+ if (!beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.Minimal).Any() && beatmaps.StableInstallationAvailable)
{
dialogOverlay.Push(new ImportFromStablePopup(() =>
{
@@ -337,13 +342,17 @@ namespace osu.Game.Screens.Select
/// Call to make a selection and perform the default action for this SongSelect.
///
/// An optional beatmap to override the current carousel selection.
- /// Whether to trigger .
- public void FinaliseSelection(BeatmapInfo beatmap = null, bool performStartAction = true)
+ /// An optional ruleset to override the current carousel selection.
+ /// An optional custom action to perform instead of .
+ public void FinaliseSelection(BeatmapInfo beatmap = null, RulesetInfo ruleset = null, Action customStartAction = null)
{
// This is very important as we have not yet bound to screen-level bindables before the carousel load is completed.
if (!Carousel.BeatmapSetsLoaded)
return;
+ if (ruleset != null)
+ Ruleset.Value = ruleset;
+
transferRulesetValue();
// while transferRulesetValue will flush, it only does so if the ruleset changes.
@@ -364,7 +373,12 @@ namespace osu.Game.Screens.Select
selectionChangedDebounce = null;
}
- if (performStartAction && OnStart())
+ if (customStartAction != null)
+ {
+ customStartAction();
+ Carousel.AllowSelection = false;
+ }
+ else if (OnStart())
Carousel.AllowSelection = false;
}
@@ -793,7 +807,7 @@ namespace osu.Game.Screens.Select
Masking = true;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
- Width = panel_overflow; //avoid horizontal masking so the panels don't clip when screen stack is pushed.
+ Width = panel_overflow; // avoid horizontal masking so the panels don't clip when screen stack is pushed.
InternalChild = Content = new Container
{
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs
index 96e3c037a3..a7c84bf692 100644
--- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs
+++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs
@@ -23,6 +23,7 @@ namespace osu.Game.Tests.Beatmaps
HitObjects = baseBeatmap.HitObjects;
BeatmapInfo.Ruleset = ruleset;
+ BeatmapInfo.RulesetID = ruleset.ID ?? 0;
BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata;
BeatmapInfo.BeatmapSet.Beatmaps = new List { BeatmapInfo };
BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo
diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs
index 8f8afb87d4..cdf9170701 100644
--- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs
+++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs
@@ -27,6 +27,10 @@ namespace osu.Game.Tests.Beatmaps
this.storyboard = storyboard;
}
+ public override bool TrackLoaded => true;
+
+ public override bool BeatmapLoaded => true;
+
protected override IBeatmap GetBeatmap() => beatmap;
protected override Storyboard GetStoryboard() => storyboard ?? base.GetStoryboard();
diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs
index 58a443ed3d..830e6ed363 100644
--- a/osu.Game/Tests/Visual/EditorClockTestScene.cs
+++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Tests.Visual
/// Provides a clock, beat-divisor, and scrolling capability for test cases of editor components that
/// are preferrably tested within the presence of a clock and seek controls.
///
- public abstract class EditorClockTestScene : OsuTestScene
+ public abstract class EditorClockTestScene : OsuManualInputManagerTestScene
{
protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor();
protected new readonly EditorClock Clock;
diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs
index 80bc3bdb87..caf2bc0ff1 100644
--- a/osu.Game/Tests/Visual/EditorTestScene.cs
+++ b/osu.Game/Tests/Visual/EditorTestScene.cs
@@ -3,9 +3,13 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Testing;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Compose.Components.Timeline;
namespace osu.Game.Tests.Visual
{
@@ -13,6 +17,8 @@ namespace osu.Game.Tests.Visual
{
public override IReadOnlyList RequiredTypes => new[] { typeof(Editor), typeof(EditorScreen) };
+ protected Editor Editor { get; private set; }
+
private readonly Ruleset ruleset;
protected EditorTestScene(Ruleset ruleset)
@@ -30,7 +36,11 @@ namespace osu.Game.Tests.Visual
{
base.SetUpSteps();
- AddStep("Load editor", () => LoadScreen(new Editor()));
+ AddStep("load editor", () => LoadScreen(Editor = CreateEditor()));
+ AddUntilStep("wait for editor to load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true
+ && Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
}
+
+ protected virtual Editor CreateEditor() => new Editor();
}
}
diff --git a/osu.Game/Tests/Visual/ScrollingTestContainer.cs b/osu.Game/Tests/Visual/ScrollingTestContainer.cs
index 3b741fcf1d..994f23577d 100644
--- a/osu.Game/Tests/Visual/ScrollingTestContainer.cs
+++ b/osu.Game/Tests/Visual/ScrollingTestContainer.cs
@@ -30,6 +30,11 @@ namespace osu.Game.Tests.Visual
set => scrollingInfo.TimeRange.Value = value;
}
+ public ScrollingDirection Direction
+ {
+ set => scrollingInfo.Direction.Value = value;
+ }
+
public IScrollingInfo ScrollingInfo => scrollingInfo;
[Cached(Type = typeof(IScrollingInfo))]
diff --git a/osu.Game/Utils/PeriodTracker.cs b/osu.Game/Utils/PeriodTracker.cs
new file mode 100644
index 0000000000..ba77702247
--- /dev/null
+++ b/osu.Game/Utils/PeriodTracker.cs
@@ -0,0 +1,69 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace osu.Game.Utils
+{
+ ///
+ /// Represents a tracking component used for whether a specific time instant falls into any of the provided periods.
+ ///
+ public class PeriodTracker
+ {
+ private readonly List periods;
+ private int nearestIndex;
+
+ public PeriodTracker(IEnumerable periods)
+ {
+ this.periods = periods.OrderBy(period => period.Start).ToList();
+ }
+
+ ///
+ /// Whether the provided time is in any of the added periods.
+ ///
+ /// The time value to check.
+ public bool IsInAny(double time)
+ {
+ if (periods.Count == 0)
+ return false;
+
+ if (time > periods[nearestIndex].End)
+ {
+ while (time > periods[nearestIndex].End && nearestIndex < periods.Count - 1)
+ nearestIndex++;
+ }
+ else
+ {
+ while (time < periods[nearestIndex].Start && nearestIndex > 0)
+ nearestIndex--;
+ }
+
+ var nearest = periods[nearestIndex];
+ return time >= nearest.Start && time <= nearest.End;
+ }
+ }
+
+ public readonly struct Period
+ {
+ ///
+ /// The start time of this period.
+ ///
+ public readonly double Start;
+
+ ///
+ /// The end time of this period.
+ ///
+ public readonly double End;
+
+ public Period(double start, double end)
+ {
+ if (start >= end)
+ throw new ArgumentException($"Invalid period provided, {nameof(start)} must be less than {nameof(end)}");
+
+ Start = start;
+ End = end;
+ }
+ }
+}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 9c17c453a6..5ccfaaac9e 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -18,13 +18,14 @@
+
-
+
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 07ea4b9c2a..dc83d937f7 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,17 +70,17 @@
-
-
+
+
-
+
-
+