diff --git a/.idea/.idea.osu.Desktop/.idea/modules.xml b/.idea/.idea.osu.Desktop/.idea/modules.xml
index fe63f5faf3..680312ad27 100644
--- a/.idea/.idea.osu.Desktop/.idea/modules.xml
+++ b/.idea/.idea.osu.Desktop/.idea/modules.xml
@@ -2,7 +2,7 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 7c749f3422..86c42dae12 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,8 @@ If you are looking to install or test osu! without setting up a development envi
| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- | ------------- |
+- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
+
- When running on Windows 7 or 8.1, **[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/dependencies?tabs=netcore31&pivots=os-windows)** may be required to correctly run .NET Core applications if your operating system is not up-to-date with the latest service packs.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
diff --git a/osu.Android.props b/osu.Android.props
index d7817cf4cf..2d531cf01e 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs
new file mode 100644
index 0000000000..25bd659a5d
--- /dev/null
+++ b/osu.Android/GameplayScreenRotationLocker.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Android.Content.PM;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game;
+
+namespace osu.Android
+{
+ public class GameplayScreenRotationLocker : Component
+ {
+ private Bindable localUserPlaying;
+
+ [Resolved]
+ private OsuGameActivity gameActivity { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuGame game)
+ {
+ localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
+ localUserPlaying.BindValueChanged(updateLock, true);
+ }
+
+ private void updateLock(ValueChangedEvent userPlaying)
+ {
+ gameActivity.RunOnUiThread(() =>
+ {
+ gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser;
+ });
+ }
+ }
+}
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index db73bb7e7f..7e250dce0e 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -12,7 +12,7 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
public class OsuGameActivity : AndroidGameActivity
{
- protected override Framework.Game CreateGame() => new OsuGameAndroid();
+ protected override Framework.Game CreateGame() => new OsuGameAndroid(this);
protected override void OnCreate(Bundle savedInstanceState)
{
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index 7542a2b997..21d6336b2c 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -4,6 +4,7 @@
using System;
using Android.App;
using Android.OS;
+using osu.Framework.Allocation;
using osu.Game;
using osu.Game.Updater;
@@ -11,6 +12,15 @@ namespace osu.Android
{
public class OsuGameAndroid : OsuGame
{
+ [Cached]
+ private readonly OsuGameActivity gameActivity;
+
+ public OsuGameAndroid(OsuGameActivity activity)
+ : base(null)
+ {
+ gameActivity = activity;
+ }
+
public override Version AssemblyVersion
{
get
@@ -55,6 +65,12 @@ namespace osu.Android
}
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
+ }
+
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
}
}
diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj
index 0598a50530..a2638e95c8 100644
--- a/osu.Android/osu.Android.csproj
+++ b/osu.Android/osu.Android.csproj
@@ -21,6 +21,7 @@
r8
+
@@ -53,4 +54,4 @@
-
+
\ No newline at end of file
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index 2079f136d2..836b968a67 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -125,12 +125,14 @@ namespace osu.Desktop
{
base.SetHost(host);
+ var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
+
switch (host.Window)
{
// Legacy osuTK DesktopGameWindow
case DesktopGameWindow desktopGameWindow:
desktopGameWindow.CursorState |= CursorState.Hidden;
- desktopGameWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"));
+ desktopGameWindow.SetIconFromStream(iconStream);
desktopGameWindow.Title = Name;
desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames);
break;
@@ -138,6 +140,7 @@ namespace osu.Desktop
// SDL2 DesktopWindow
case DesktopWindow desktopWindow:
desktopWindow.CursorState.Value |= CursorState.Hidden;
+ desktopWindow.SetIconFromStream(iconStream);
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });
break;
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 05c8e835ac..71f9fafe57 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -29,6 +29,11 @@ namespace osu.Desktop.Updater
private static readonly Logger logger = Logger.GetLogger("updater");
+ ///
+ /// Whether an update has been downloaded but not yet applied.
+ ///
+ private bool updatePending;
+
[BackgroundDependencyLoader]
private void load(NotificationOverlay notification)
{
@@ -37,9 +42,9 @@ namespace osu.Desktop.Updater
Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
}
- protected override async Task PerformUpdateCheck() => await checkForUpdateAsync();
+ protected override async Task PerformUpdateCheck() => await checkForUpdateAsync();
- private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
+ private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
{
// should we schedule a retry on completion of this check?
bool scheduleRecheck = true;
@@ -49,9 +54,19 @@ namespace osu.Desktop.Updater
updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true);
var info = await updateManager.CheckForUpdate(!useDeltaPatching);
+
if (info.ReleasesToApply.Count == 0)
+ {
+ if (updatePending)
+ {
+ // the user may have dismissed the completion notice, so show it again.
+ notificationOverlay.Post(new UpdateCompleteNotification(this));
+ return true;
+ }
+
// no updates available. bail and retry later.
- return;
+ return false;
+ }
if (notification == null)
{
@@ -72,6 +87,7 @@ namespace osu.Desktop.Updater
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f);
notification.State = ProgressNotificationState.Completed;
+ updatePending = true;
}
catch (Exception e)
{
@@ -103,6 +119,8 @@ namespace osu.Desktop.Updater
Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30);
}
}
+
+ return true;
}
protected override void Dispose(bool isDisposing)
@@ -111,10 +129,27 @@ namespace osu.Desktop.Updater
updateManager?.Dispose();
}
+ private class UpdateCompleteNotification : ProgressCompletionNotification
+ {
+ [Resolved]
+ private OsuGame game { get; set; }
+
+ public UpdateCompleteNotification(SquirrelUpdateManager updateManager)
+ {
+ Text = @"Update ready to install. Click to restart!";
+
+ Activated = () =>
+ {
+ updateManager.PrepareUpdateAsync()
+ .ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
+ return true;
+ };
+ }
+ }
+
private class UpdateProgressNotification : ProgressNotification
{
private readonly SquirrelUpdateManager updateManager;
- private OsuGame game;
public UpdateProgressNotification(SquirrelUpdateManager updateManager)
{
@@ -123,23 +158,12 @@ namespace osu.Desktop.Updater
protected override Notification CreateCompletionNotification()
{
- return new ProgressCompletionNotification
- {
- Text = @"Update ready to install. Click to restart!",
- Activated = () =>
- {
- updateManager.PrepareUpdateAsync()
- .ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
- return true;
- }
- };
+ return new UpdateCompleteNotification(updateManager);
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours, OsuGame game)
+ private void load(OsuColour colours)
{
- this.game = game;
-
IconContent.AddRange(new Drawable[]
{
new Box
diff --git a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
index 86174ceb90..efc3f21149 100644
--- a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
+++ b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
@@ -5,24 +5,24 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Platform;
+using osu.Game;
using osu.Game.Configuration;
namespace osu.Desktop.Windows
{
public class GameplayWinKeyBlocker : Component
{
- private Bindable allowScreenSuspension;
private Bindable disableWinKey;
+ private Bindable localUserPlaying;
- private GameHost host;
+ [Resolved]
+ private GameHost host { get; set; }
[BackgroundDependencyLoader]
- private void load(GameHost host, OsuConfigManager config)
+ private void load(OsuGame game, OsuConfigManager config)
{
- this.host = host;
-
- allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy();
- allowScreenSuspension.BindValueChanged(_ => updateBlocking());
+ localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
+ localUserPlaying.BindValueChanged(_ => updateBlocking());
disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey);
disableWinKey.BindValueChanged(_ => updateBlocking(), true);
@@ -30,7 +30,7 @@ namespace osu.Desktop.Windows
private void updateBlocking()
{
- bool shouldDisable = disableWinKey.Value && !allowScreenSuspension.Value;
+ bool shouldDisable = disableWinKey.Value && localUserPlaying.Value;
if (shouldDisable)
host.InputThread.Scheduler.Add(WindowsKey.Disable);
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
index 1e708cce4b..1b8368794c 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -123,7 +123,10 @@ namespace osu.Game.Rulesets.Catch.Tests
Origin = Anchor.Centre,
Scale = new Vector2(4f),
}, skin);
+ });
+ AddStep("get trails container", () =>
+ {
trails = catcherArea.OfType().Single();
catcherArea.MovableCatcher.SetHyperDashState(2);
});
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index ca75a816f1..ad584d3f48 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -141,11 +141,40 @@ namespace osu.Game.Rulesets.Catch
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
+ protected override IEnumerable GetValidHitResults()
+ {
+ return new[]
+ {
+ HitResult.Great,
+
+ HitResult.LargeTickHit,
+ HitResult.SmallTickHit,
+ HitResult.LargeBonus,
+ };
+ }
+
+ public override string GetDisplayNameForHitResult(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.LargeTickHit:
+ return "large droplet";
+
+ case HitResult.SmallTickHit:
+ return "small droplet";
+
+ case HitResult.LargeBonus:
+ return "banana";
+ }
+
+ return base.GetDisplayNameForHitResult(result);
+ }
+
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
- public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score);
+ public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
public int LegacyID => 2;
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index a4b9ca35eb..6a3a16ed33 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
-using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@@ -25,8 +24,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private int tinyTicksMissed;
private int misses;
- public CatchPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
- : base(ruleset, beatmap, score)
+ public CatchPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
+ : base(ruleset, attributes, score)
{
}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 6b8b70ed54..e209d012fa 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.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 System.Linq;
using System.Threading;
@@ -56,6 +57,7 @@ namespace osu.Game.Rulesets.Catch.Objects
Volume = s.Volume
}).ToList();
+ int nodeIndex = 0;
SliderEventDescriptor? lastEvent = null;
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
@@ -105,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Objects
case SliderEventType.Repeat:
AddNested(new Fruit
{
- Samples = Samples,
+ Samples = this.GetNodeSamples(nodeIndex++),
StartTime = e.Time,
X = X + Path.PositionAt(e.PathProgress).X,
});
@@ -119,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Duration
{
get => this.SpanCount() * Path.Distance / Velocity;
- set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
+ set => throw new NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}
public double EndTime => StartTime + Duration;
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
index 47224bd195..22db147e32 100644
--- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
@@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Catch.Skinning
{
public class CatchLegacySkinTransformer : LegacySkinTransformer
{
+ ///
+ /// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
+ ///
+ private bool providesComboCounter => this.HasFont(GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score");
+
public CatchLegacySkinTransformer(ISkinSource source)
: base(source)
{
@@ -20,6 +25,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
public override Drawable GetDrawableComponent(ISkinComponent component)
{
+ if (component is HUDSkinComponent hudComponent)
+ {
+ switch (hudComponent.Component)
+ {
+ case HUDSkinComponents.ComboCounter:
+ // catch may provide its own combo counter; hide the default.
+ return providesComboCounter ? Drawable.Empty() : null;
+ }
+ }
+
if (!(component is CatchSkinComponent catchSkinComponent))
return null;
@@ -55,11 +70,9 @@ namespace osu.Game.Rulesets.Catch.Skinning
this.GetAnimation("fruit-ryuuta", true, true, true);
case CatchSkinComponents.CatchComboCounter:
- var comboFont = GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score";
- // For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
- if (this.HasFont(comboFont))
- return new LegacyComboCounter(Source);
+ if (providesComboCounter)
+ return new LegacyCatchComboCounter(Source);
break;
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyCatchComboCounter.cs
similarity index 96%
rename from osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs
rename to osu.Game.Rulesets.Catch/Skinning/LegacyCatchComboCounter.cs
index c8abc9e832..34608b07ff 100644
--- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/LegacyCatchComboCounter.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Skinning
///
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
///
- public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter
+ public class LegacyCatchComboCounter : CompositeDrawable, ICatchComboCounter
{
private readonly LegacyRollingCounter counter;
private readonly LegacyRollingCounter explosion;
- public LegacyComboCounter(ISkin skin)
+ public LegacyCatchComboCounter(ISkin skin)
{
var fontName = skin.GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score";
var fontOverlap = skin.GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f;
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 9289a6162c..a221ca7966 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -145,11 +145,19 @@ namespace osu.Game.Rulesets.Catch.UI
}
};
- trailsTarget.Add(trails = new CatcherTrailDisplay(this));
+ trails = new CatcherTrailDisplay(this);
updateCatcher();
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // don't add in above load as we may potentially modify a parent in an unsafe manner.
+ trailsTarget.Add(trails);
+ }
+
///
/// Creates proxied content to be displayed beneath hitobjects.
///
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
index 0c57267970..3d4bc4748b 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
@@ -83,11 +83,17 @@ namespace osu.Game.Rulesets.Mania.Tests
RandomZ = snapshot.RandomZ;
}
+ public override void PostProcess()
+ {
+ base.PostProcess();
+ Objects.Sort();
+ }
+
public bool Equals(ManiaConvertMapping other) => other != null && RandomW == other.RandomW && RandomX == other.RandomX && RandomY == other.RandomY && RandomZ == other.RandomZ;
public override bool Equals(ConvertMapping other) => base.Equals(other) && Equals(other as ManiaConvertMapping);
}
- public struct ConvertValue : IEquatable
+ public struct ConvertValue : IEquatable, IComparable
{
///
/// A sane value to account for osu!stable using ints everwhere.
@@ -102,5 +108,15 @@ namespace osu.Game.Rulesets.Mania.Tests
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
&& Column == other.Column;
+
+ public int CompareTo(ConvertValue other)
+ {
+ var result = StartTime.CompareTo(other.StartTime);
+
+ if (result != 0)
+ return result;
+
+ return Column.CompareTo(other.Column);
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
index 2c36e81190..a25551f854 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
- [TestCase(2.3683365342338796d, "diffcalc-test")]
+ [TestCase(2.3449735700206298d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
index d1d5adea75..93a9ce3dbd 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
@@ -21,13 +21,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
///
public int TotalColumns => Stages.Sum(g => g.Columns);
+ ///
+ /// The total number of columns that were present in this before any user adjustments.
+ ///
+ public readonly int OriginalTotalColumns;
+
///
/// Creates a new .
///
/// The initial stages.
- public ManiaBeatmap(StageDefinition defaultStage)
+ /// The total number of columns present before any user adjustments. Defaults to the total columns in .
+ public ManiaBeatmap(StageDefinition defaultStage, int? originalTotalColumns = null)
{
Stages.Add(defaultStage);
+ OriginalTotalColumns = originalTotalColumns ?? defaultStage.Columns;
}
public override IEnumerable GetStatistics()
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index 524ea27efa..7a0e3b2b76 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
public bool Dual;
public readonly bool IsForCurrentRuleset;
+ private readonly int originalTargetColumns;
+
// Internal for testing purposes
internal FastRandom Random { get; private set; }
@@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
else
TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
}
+
+ originalTargetColumns = TargetColumns;
}
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
@@ -81,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override Beatmap CreateBeatmap()
{
- beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns });
+ beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }, originalTargetColumns);
if (Dual)
beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns });
@@ -116,7 +120,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
prevNoteTimes.RemoveAt(0);
prevNoteTimes.Add(newNoteTime);
- density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
+ if (prevNoteTimes.Count >= 2)
+ density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
}
private double lastTime;
@@ -180,7 +185,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
case IHasDuration endTimeData:
{
- conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap);
+ conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
recordNote(endTimeData.EndTime, new Vector2(256, 192));
computeDensity(endTimeData.EndTime);
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index fe146c5324..30d33de06e 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -3,8 +3,8 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
-using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
@@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Formats;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
@@ -25,8 +26,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
private const float osu_base_scoring_distance = 100;
- public readonly double EndTime;
- public readonly double SegmentDuration;
+ public readonly int StartTime;
+ public readonly int EndTime;
+ public readonly int SegmentDuration;
public readonly int SpanCount;
private PatternType convertType;
@@ -41,20 +43,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var distanceData = hitObject as IHasDistance;
var repeatsData = hitObject as IHasRepeats;
- SpanCount = repeatsData?.SpanCount() ?? 1;
+ Debug.Assert(distanceData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
- // The true distance, accounting for any repeats
- double distance = (distanceData?.Distance ?? 0) * SpanCount;
- // The velocity of the osu! hit object - calculated as the velocity of a slider
- double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength;
- // The duration of the osu! hit object
- double osuDuration = distance / osuVelocity;
+ double beatLength;
+#pragma warning disable 618
+ if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
+#pragma warning restore 618
+ beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
+ else
+ beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
- EndTime = hitObject.StartTime + osuDuration;
- SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount;
+ SpanCount = repeatsData?.SpanCount() ?? 1;
+ StartTime = (int)Math.Round(hitObject.StartTime);
+
+ // This matches stable's calculation.
+ EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier);
+
+ SegmentDuration = (EndTime - StartTime) / SpanCount;
}
public override IEnumerable Generate()
@@ -76,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
foreach (var obj in originalPattern.HitObjects)
{
- if (!Precision.AlmostEquals(EndTime, obj.GetEndTime()))
+ if (EndTime != (int)Math.Round(obj.GetEndTime()))
intermediatePattern.Add(obj);
else
endTimePattern.Add(obj);
@@ -91,35 +99,35 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (TotalColumns == 1)
{
var pattern = new Pattern();
- addToPattern(pattern, 0, HitObject.StartTime, EndTime);
+ addToPattern(pattern, 0, StartTime, EndTime);
return pattern;
}
if (SpanCount > 1)
{
if (SegmentDuration <= 90)
- return generateRandomHoldNotes(HitObject.StartTime, 1);
+ return generateRandomHoldNotes(StartTime, 1);
if (SegmentDuration <= 120)
{
convertType |= PatternType.ForceNotStack;
- return generateRandomNotes(HitObject.StartTime, SpanCount + 1);
+ return generateRandomNotes(StartTime, SpanCount + 1);
}
if (SegmentDuration <= 160)
- return generateStair(HitObject.StartTime);
+ return generateStair(StartTime);
if (SegmentDuration <= 200 && ConversionDifficulty > 3)
- return generateRandomMultipleNotes(HitObject.StartTime);
+ return generateRandomMultipleNotes(StartTime);
- double duration = EndTime - HitObject.StartTime;
+ double duration = EndTime - StartTime;
if (duration >= 4000)
- return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0);
+ return generateNRandomNotes(StartTime, 0.23, 0, 0);
if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart)
- return generateTiledHoldNotes(HitObject.StartTime);
+ return generateTiledHoldNotes(StartTime);
- return generateHoldAndNormalNotes(HitObject.StartTime);
+ return generateHoldAndNormalNotes(StartTime);
}
if (SegmentDuration <= 110)
@@ -128,37 +136,37 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
convertType |= PatternType.ForceNotStack;
else
convertType &= ~PatternType.ForceNotStack;
- return generateRandomNotes(HitObject.StartTime, SegmentDuration < 80 ? 1 : 2);
+ return generateRandomNotes(StartTime, SegmentDuration < 80 ? 1 : 2);
}
if (ConversionDifficulty > 6.5)
{
if (convertType.HasFlag(PatternType.LowProbability))
- return generateNRandomNotes(HitObject.StartTime, 0.78, 0.3, 0);
+ return generateNRandomNotes(StartTime, 0.78, 0.3, 0);
- return generateNRandomNotes(HitObject.StartTime, 0.85, 0.36, 0.03);
+ return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03);
}
if (ConversionDifficulty > 4)
{
if (convertType.HasFlag(PatternType.LowProbability))
- return generateNRandomNotes(HitObject.StartTime, 0.43, 0.08, 0);
+ return generateNRandomNotes(StartTime, 0.43, 0.08, 0);
- return generateNRandomNotes(HitObject.StartTime, 0.56, 0.18, 0);
+ return generateNRandomNotes(StartTime, 0.56, 0.18, 0);
}
if (ConversionDifficulty > 2.5)
{
if (convertType.HasFlag(PatternType.LowProbability))
- return generateNRandomNotes(HitObject.StartTime, 0.3, 0, 0);
+ return generateNRandomNotes(StartTime, 0.3, 0, 0);
- return generateNRandomNotes(HitObject.StartTime, 0.37, 0.08, 0);
+ return generateNRandomNotes(StartTime, 0.37, 0.08, 0);
}
if (convertType.HasFlag(PatternType.LowProbability))
- return generateNRandomNotes(HitObject.StartTime, 0.17, 0, 0);
+ return generateNRandomNotes(StartTime, 0.17, 0, 0);
- return generateNRandomNotes(HitObject.StartTime, 0.27, 0, 0);
+ return generateNRandomNotes(StartTime, 0.27, 0, 0);
}
///
@@ -167,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// Start time of each hold note.
/// Number of hold notes.
/// The containing the hit objects.
- private Pattern generateRandomHoldNotes(double startTime, int noteCount)
+ private Pattern generateRandomHoldNotes(int startTime, int noteCount)
{
// - - - -
// ■ - ■ ■
@@ -202,7 +210,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// The start time.
/// The number of notes.
/// The containing the hit objects.
- private Pattern generateRandomNotes(double startTime, int noteCount)
+ private Pattern generateRandomNotes(int startTime, int noteCount)
{
// - - - -
// x - - -
@@ -234,7 +242,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// The start time.
/// The containing the hit objects.
- private Pattern generateStair(double startTime)
+ private Pattern generateStair(int startTime)
{
// - - - -
// x - - -
@@ -286,7 +294,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// The start time.
/// The containing the hit objects.
- private Pattern generateRandomMultipleNotes(double startTime)
+ private Pattern generateRandomMultipleNotes(int startTime)
{
// - - - -
// x - - -
@@ -329,7 +337,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// The probability required for 3 hold notes to be generated.
/// The probability required for 4 hold notes to be generated.
/// The containing the hit objects.
- private Pattern generateNRandomNotes(double startTime, double p2, double p3, double p4)
+ private Pattern generateNRandomNotes(int startTime, double p2, double p3, double p4)
{
// - - - -
// ■ - ■ ■
@@ -366,7 +374,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
- canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample);
+ canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample);
if (canGenerateTwoNotes)
p2 = 1;
@@ -379,7 +387,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// The first hold note start time.
/// The containing the hit objects.
- private Pattern generateTiledHoldNotes(double startTime)
+ private Pattern generateTiledHoldNotes(int startTime)
{
// - - - -
// ■ ■ ■ ■
@@ -394,6 +402,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int columnRepeat = Math.Min(SpanCount, TotalColumns);
+ // Due to integer rounding, this is not guaranteed to be the same as EndTime (the class-level variable).
+ int endTime = startTime + SegmentDuration * SpanCount;
+
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
@@ -401,7 +412,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
for (int i = 0; i < columnRepeat; i++)
{
nextColumn = FindAvailableColumn(nextColumn, pattern);
- addToPattern(pattern, nextColumn, startTime, EndTime);
+ addToPattern(pattern, nextColumn, startTime, endTime);
startTime += SegmentDuration;
}
@@ -413,7 +424,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// The start time of notes.
/// The containing the hit objects.
- private Pattern generateHoldAndNormalNotes(double startTime)
+ private Pattern generateHoldAndNormalNotes(int startTime)
{
// - - - -
// ■ x x -
@@ -448,7 +459,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
for (int i = 0; i <= SpanCount; i++)
{
- if (!(ignoreHead && startTime == HitObject.StartTime))
+ if (!(ignoreHead && startTime == StartTime))
{
for (int j = 0; j < noteCount; j++)
{
@@ -471,19 +482,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// The time to retrieve the sample info list from.
///
- private IList sampleInfoListAt(double time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
+ private IList sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
///
/// Retrieves the list of node samples that occur at time greater than or equal to .
///
/// The time to retrieve node samples at.
- private List> nodeSamplesAt(double time)
+ private List> nodeSamplesAt(int time)
{
if (!(HitObject is IHasPathWithRepeats curveData))
return null;
- // mathematically speaking this should be a whole number always, but floating-point arithmetic is not so kind
- var index = (int)Math.Round(SegmentDuration == 0 ? 0 : (time - HitObject.StartTime) / SegmentDuration, MidpointRounding.AwayFromZero);
+ var index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
// avoid slicing the list & creating copies, if at all possible.
return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
@@ -496,7 +506,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// The column to add the note to.
/// The start time of the note.
/// The end time of the note (set to for a non-hold note).
- private void addToPattern(Pattern pattern, int column, double startTime, double endTime)
+ private void addToPattern(Pattern pattern, int column, int startTime, int endTime)
{
ManiaHitObject newObject;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
index d5286a3779..f816a70ab3 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
@@ -14,12 +14,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
internal class EndTimeObjectPatternGenerator : PatternGenerator
{
- private readonly double endTime;
+ private readonly int endTime;
+ private readonly PatternType convertType;
- public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, IBeatmap originalBeatmap)
- : base(random, hitObject, beatmap, new Pattern(), originalBeatmap)
+ public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
+ : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{
- endTime = (HitObject as IHasDuration)?.EndTime ?? 0;
+ endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);
+
+ convertType = PreviousPattern.ColumnWithObjects == TotalColumns
+ ? PatternType.None
+ : PatternType.ForceNotStack;
}
public override IEnumerable Generate()
@@ -40,18 +45,25 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
break;
case 8:
- addToPattern(pattern, FindAvailableColumn(GetRandomColumn(), PreviousPattern), generateHold);
+ addToPattern(pattern, getRandomColumn(), generateHold);
break;
default:
- if (TotalColumns > 0)
- addToPattern(pattern, GetRandomColumn(), generateHold);
+ addToPattern(pattern, getRandomColumn(0), generateHold);
break;
}
return pattern;
}
+ private int getRandomColumn(int? lowerBound = null)
+ {
+ if ((convertType & PatternType.ForceNotStack) > 0)
+ return FindAvailableColumn(GetRandomColumn(lowerBound), lowerBound, patterns: PreviousPattern);
+
+ return FindAvailableColumn(GetRandomColumn(lowerBound), lowerBound);
+ }
+
///
/// Constructs and adds a note to a pattern.
///
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
index 84f950997d..bc4ab55767 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
@@ -397,7 +397,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
case 4:
centreProbability = 0;
- p2 = Math.Min(p2 * 2, 0.2);
+
+ // Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x).
+ // But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer),
+ // so it needs to be converted to from a probability and then back after the multiplication.
+ p2 = 1 - Math.Max((1 - p2) * 2, 0.8);
p3 = 0;
break;
@@ -408,11 +412,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
case 6:
centreProbability = 0;
- p2 = Math.Min(p2 * 2, 0.5);
- p3 = Math.Min(p3 * 2, 0.15);
+
+ // Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x).
+ // But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer),
+ // so it needs to be converted to from a probability and then back after the multiplication.
+ p2 = 1 - Math.Max((1 - p2) * 2, 0.5);
+ p3 = 1 - Math.Max((1 - p3) * 2, 0.85);
break;
}
+ // The stable values were allowed to exceed 1, which indicate <0% probability.
+ // These values needs to be clamped otherwise GetRandomNoteCount() will throw an exception.
+ p2 = Math.Clamp(p2, 0, 1);
+ p3 = Math.Clamp(p3, 0, 1);
+
double centreVal = Random.NextDouble();
int noteCount = GetRandomNoteCount(p2, p3);
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
index 3ff665d2c8..0b58d1efc6 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
@@ -8,5 +8,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
public class ManiaDifficultyAttributes : DifficultyAttributes
{
public double GreatHitWindow;
+ public double ScoreMultiplier;
}
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index b08c520c54..ade830764d 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.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 System.Linq;
using osu.Game.Beatmaps;
@@ -10,10 +11,12 @@ using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Difficulty.Skills;
+using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Difficulty
@@ -23,11 +26,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private const double star_scaling_factor = 0.018;
private readonly bool isForCurrentRuleset;
+ private readonly double originalOverallDifficulty;
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
+ originalOverallDifficulty = beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
@@ -40,64 +45,33 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return new ManiaDifficultyAttributes
{
- StarRating = difficultyValue(skills) * star_scaling_factor,
+ StarRating = skills[0].DifficultyValue() * star_scaling_factor,
Mods = mods,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
- GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate,
+ GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
+ ScoreMultiplier = getScoreMultiplier(beatmap, mods),
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills
};
}
- private double difficultyValue(Skill[] skills)
- {
- // Preprocess the strains to find the maximum overall + individual (aggregate) strain from each section
- var overall = skills.OfType().Single();
- var aggregatePeaks = new List(Enumerable.Repeat(0.0, overall.StrainPeaks.Count));
-
- foreach (var individual in skills.OfType())
- {
- for (int i = 0; i < individual.StrainPeaks.Count; i++)
- {
- double aggregate = individual.StrainPeaks[i] + overall.StrainPeaks[i];
-
- if (aggregate > aggregatePeaks[i])
- aggregatePeaks[i] = aggregate;
- }
- }
-
- aggregatePeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
-
- double difficulty = 0;
- double weight = 1;
-
- // Difficulty is the weighted sum of the highest strains from every section.
- foreach (double strain in aggregatePeaks)
- {
- difficulty += strain * weight;
- weight *= 0.9;
- }
-
- return difficulty;
- }
-
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
- for (int i = 1; i < beatmap.HitObjects.Count; i++)
- yield return new ManiaDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
+ var sortedObjects = beatmap.HitObjects.ToArray();
+
+ LegacySortHelper.Sort(sortedObjects, Comparer.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
+
+ for (int i = 1; i < sortedObjects.Length; i++)
+ yield return new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate);
}
- protected override Skill[] CreateSkills(IBeatmap beatmap)
+ // Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
+ protected override IEnumerable SortObjects(IEnumerable input) => input;
+
+ protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
{
- int columnCount = ((ManiaBeatmap)beatmap).TotalColumns;
-
- var skills = new List { new Overall(columnCount) };
-
- for (int i = 0; i < columnCount; i++)
- skills.Add(new Individual(i, columnCount));
-
- return skills.ToArray();
- }
+ new Strain(((ManiaBeatmap)beatmap).TotalColumns)
+ };
protected override Mod[] DifficultyAdjustmentMods
{
@@ -122,12 +96,73 @@ namespace osu.Game.Rulesets.Mania.Difficulty
new ManiaModKey3(),
new ManiaModKey4(),
new ManiaModKey5(),
+ new MultiMod(new ManiaModKey5(), new ManiaModDualStages()),
new ManiaModKey6(),
+ new MultiMod(new ManiaModKey6(), new ManiaModDualStages()),
new ManiaModKey7(),
+ new MultiMod(new ManiaModKey7(), new ManiaModDualStages()),
new ManiaModKey8(),
+ new MultiMod(new ManiaModKey8(), new ManiaModDualStages()),
new ManiaModKey9(),
+ new MultiMod(new ManiaModKey9(), new ManiaModDualStages()),
}).ToArray();
}
}
+
+ private int getHitWindow300(Mod[] mods)
+ {
+ if (isForCurrentRuleset)
+ {
+ double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty));
+ return applyModAdjustments(34 + 3 * od, mods);
+ }
+
+ if (Math.Round(originalOverallDifficulty) > 4)
+ return applyModAdjustments(34, mods);
+
+ return applyModAdjustments(47, mods);
+
+ static int applyModAdjustments(double value, Mod[] mods)
+ {
+ if (mods.Any(m => m is ManiaModHardRock))
+ value /= 1.4;
+ else if (mods.Any(m => m is ManiaModEasy))
+ value *= 1.4;
+
+ if (mods.Any(m => m is ManiaModDoubleTime))
+ value *= 1.5;
+ else if (mods.Any(m => m is ManiaModHalfTime))
+ value *= 0.75;
+
+ return (int)value;
+ }
+ }
+
+ private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods)
+ {
+ double scoreMultiplier = 1;
+
+ foreach (var m in mods)
+ {
+ switch (m)
+ {
+ case ManiaModNoFail _:
+ case ManiaModEasy _:
+ case ManiaModHalfTime _:
+ scoreMultiplier *= 0.5;
+ break;
+ }
+ }
+
+ var maniaBeatmap = (ManiaBeatmap)beatmap;
+ int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
+
+ if (diff > 0)
+ scoreMultiplier *= 0.9;
+ else if (diff < 0)
+ scoreMultiplier *= 0.9 + 0.04 * diff;
+
+ return scoreMultiplier;
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
index 91383c5548..00bec18a45 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
-using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@@ -29,8 +28,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private int countMeh;
private int countMiss;
- public ManiaPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
- : base(ruleset, beatmap, score)
+ public ManiaPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
+ : base(ruleset, attributes, score)
{
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs
deleted file mode 100644
index 4f7ab87fad..0000000000
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs
+++ /dev/null
@@ -1,47 +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 System.Linq;
-using osu.Game.Rulesets.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Difficulty.Skills;
-using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Objects;
-
-namespace osu.Game.Rulesets.Mania.Difficulty.Skills
-{
- public class Individual : Skill
- {
- protected override double SkillMultiplier => 1;
- protected override double StrainDecayBase => 0.125;
-
- private readonly double[] holdEndTimes;
-
- private readonly int column;
-
- public Individual(int column, int columnCount)
- {
- this.column = column;
-
- holdEndTimes = new double[columnCount];
- }
-
- protected override double StrainValueOf(DifficultyHitObject current)
- {
- var maniaCurrent = (ManiaDifficultyHitObject)current;
- var endTime = maniaCurrent.BaseObject.GetEndTime();
-
- try
- {
- if (maniaCurrent.BaseObject.Column != column)
- return 0;
-
- // We give a slight bonus if something is held meanwhile
- return holdEndTimes.Any(t => t > endTime) ? 2.5 : 2;
- }
- finally
- {
- holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs
deleted file mode 100644
index bbbb93fd8b..0000000000
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs
+++ /dev/null
@@ -1,56 +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.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Difficulty.Skills;
-using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Objects;
-
-namespace osu.Game.Rulesets.Mania.Difficulty.Skills
-{
- public class Overall : Skill
- {
- protected override double SkillMultiplier => 1;
- protected override double StrainDecayBase => 0.3;
-
- private readonly double[] holdEndTimes;
-
- private readonly int columnCount;
-
- public Overall(int columnCount)
- {
- this.columnCount = columnCount;
-
- holdEndTimes = new double[columnCount];
- }
-
- protected override double StrainValueOf(DifficultyHitObject current)
- {
- var maniaCurrent = (ManiaDifficultyHitObject)current;
- var endTime = maniaCurrent.BaseObject.GetEndTime();
-
- double holdFactor = 1.0; // Factor in case something else is held
- double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
-
- for (int i = 0; i < columnCount; i++)
- {
- // If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
- if (current.BaseObject.StartTime < holdEndTimes[i] && endTime > holdEndTimes[i])
- holdAddition = 1.0;
-
- // ... this addition only is valid if there is _no_ other note with the same ending.
- // Releasing multiple notes at the same time is just as easy as releasing one
- if (endTime == holdEndTimes[i])
- holdAddition = 0;
-
- // We give a slight bonus if something is held meanwhile
- if (holdEndTimes[i] > endTime)
- holdFactor = 1.25;
- }
-
- holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
-
- return (1 + holdAddition) * holdFactor;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
new file mode 100644
index 0000000000..7ebc1ff752
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
@@ -0,0 +1,80 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Rulesets.Mania.Difficulty.Skills
+{
+ public class Strain : Skill
+ {
+ private const double individual_decay_base = 0.125;
+ private const double overall_decay_base = 0.30;
+
+ protected override double SkillMultiplier => 1;
+ protected override double StrainDecayBase => 1;
+
+ private readonly double[] holdEndTimes;
+ private readonly double[] individualStrains;
+
+ private double individualStrain;
+ private double overallStrain;
+
+ public Strain(int totalColumns)
+ {
+ holdEndTimes = new double[totalColumns];
+ individualStrains = new double[totalColumns];
+ overallStrain = 1;
+ }
+
+ protected override double StrainValueOf(DifficultyHitObject current)
+ {
+ var maniaCurrent = (ManiaDifficultyHitObject)current;
+ var endTime = maniaCurrent.BaseObject.GetEndTime();
+ var column = maniaCurrent.BaseObject.Column;
+
+ double holdFactor = 1.0; // Factor to all additional strains in case something else is held
+ double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
+
+ // Fill up the holdEndTimes array
+ for (int i = 0; i < holdEndTimes.Length; ++i)
+ {
+ // If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
+ if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.BaseObject.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1))
+ holdAddition = 1.0;
+
+ // ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
+ if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1))
+ holdAddition = 0;
+
+ // We give a slight bonus to everything if something is held meanwhile
+ if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))
+ holdFactor = 1.25;
+
+ // Decay individual strains
+ individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
+ }
+
+ holdEndTimes[column] = endTime;
+
+ // Increase individual strain in own column
+ individualStrains[column] += 2.0 * holdFactor;
+ individualStrain = individualStrains[column];
+
+ overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor;
+
+ return individualStrain + overallStrain - CurrentStrain;
+ }
+
+ protected override double GetPeakStrain(double offset)
+ => applyDecay(individualStrain, offset - Previous[0].BaseObject.StartTime, individual_decay_base)
+ + applyDecay(overallStrain, offset - Previous[0].BaseObject.StartTime, overall_decay_base);
+
+ private double applyDecay(double value, double deltaTime, double decayBase)
+ => value * Math.Pow(decayBase, deltaTime / 1000);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 65f40d7d0a..50629f41a9 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit
int minColumn = int.MaxValue;
int maxColumn = int.MinValue;
- foreach (var obj in SelectedHitObjects.OfType())
+ foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType())
{
if (obj.Column < minColumn)
minColumn = obj.Column;
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Edit
columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn);
- foreach (var obj in SelectedHitObjects.OfType())
+ foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType())
obj.Column += columnDelta;
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 71ac85dd1b..b92e042686 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
- public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new ManiaPerformanceCalculator(this, beatmap, score);
+ public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new ManiaPerformanceCalculator(this, attributes, score);
public const string SHORT_NAME = "mania";
@@ -319,6 +319,31 @@ namespace osu.Game.Rulesets.Mania
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v);
}
+ protected override IEnumerable GetValidHitResults()
+ {
+ return new[]
+ {
+ HitResult.Perfect,
+ HitResult.Great,
+ HitResult.Good,
+ HitResult.Ok,
+ HitResult.Meh,
+
+ HitResult.LargeTickHit,
+ };
+ }
+
+ public override string GetDisplayNameForHitResult(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.LargeTickHit:
+ return "hold tick";
+ }
+
+ return base.GetDisplayNameForHitResult(result);
+ }
+
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{
new StatisticRow
diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
index b470405df2..de77af8306 100644
--- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
@@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Mania
new SettingsEnumDropdown
{
LabelText = "Scrolling direction",
- Bindable = config.GetBindable(ManiaRulesetSetting.ScrollDirection)
+ Current = config.GetBindable(ManiaRulesetSetting.ScrollDirection)
},
new SettingsSlider
{
LabelText = "Scroll speed",
- Bindable = config.GetBindable(ManiaRulesetSetting.ScrollTime),
+ Current = config.GetBindable(ManiaRulesetSetting.ScrollTime),
KeyboardStep = 5
},
};
diff --git a/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs b/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
new file mode 100644
index 0000000000..0f4829028f
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
@@ -0,0 +1,165 @@
+// 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.Diagnostics.Contracts;
+
+namespace osu.Game.Rulesets.Mania.MathUtils
+{
+ ///
+ /// Provides access to .NET4.0 unstable sorting methods.
+ ///
+ ///
+ /// Source: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/arraysorthelper.cs
+ /// Copyright (c) Microsoft Corporation. All rights reserved.
+ ///
+ internal static class LegacySortHelper
+ {
+ private const int quick_sort_depth_threshold = 32;
+
+ public static void Sort(T[] keys, IComparer comparer)
+ {
+ if (keys == null)
+ throw new ArgumentNullException(nameof(keys));
+
+ if (keys.Length == 0)
+ return;
+
+ comparer ??= Comparer.Default;
+ depthLimitedQuickSort(keys, 0, keys.Length - 1, comparer, quick_sort_depth_threshold);
+ }
+
+ private static void depthLimitedQuickSort(T[] keys, int left, int right, IComparer comparer, int depthLimit)
+ {
+ do
+ {
+ if (depthLimit == 0)
+ {
+ heapsort(keys, left, right, comparer);
+ return;
+ }
+
+ int i = left;
+ int j = right;
+
+ // pre-sort the low, middle (pivot), and high values in place.
+ // this improves performance in the face of already sorted data, or
+ // data that is made up of multiple sorted runs appended together.
+ int middle = i + ((j - i) >> 1);
+ swapIfGreater(keys, comparer, i, middle); // swap the low with the mid point
+ swapIfGreater(keys, comparer, i, j); // swap the low with the high
+ swapIfGreater(keys, comparer, middle, j); // swap the middle with the high
+
+ T x = keys[middle];
+
+ do
+ {
+ while (comparer.Compare(keys[i], x) < 0) i++;
+ while (comparer.Compare(x, keys[j]) < 0) j--;
+ Contract.Assert(i >= left && j <= right, "(i>=left && j<=right) Sort failed - Is your IComparer bogus?");
+ if (i > j) break;
+
+ if (i < j)
+ {
+ T key = keys[i];
+ keys[i] = keys[j];
+ keys[j] = key;
+ }
+
+ i++;
+ j--;
+ } while (i <= j);
+
+ // The next iteration of the while loop is to "recursively" sort the larger half of the array and the
+ // following calls recrusively sort the smaller half. So we subtrack one from depthLimit here so
+ // both sorts see the new value.
+ depthLimit--;
+
+ if (j - left <= right - i)
+ {
+ if (left < j) depthLimitedQuickSort(keys, left, j, comparer, depthLimit);
+ left = i;
+ }
+ else
+ {
+ if (i < right) depthLimitedQuickSort(keys, i, right, comparer, depthLimit);
+ right = j;
+ }
+ } while (left < right);
+ }
+
+ private static void heapsort(T[] keys, int lo, int hi, IComparer comparer)
+ {
+ Contract.Requires(keys != null);
+ Contract.Requires(comparer != null);
+ Contract.Requires(lo >= 0);
+ Contract.Requires(hi > lo);
+ Contract.Requires(hi < keys.Length);
+
+ int n = hi - lo + 1;
+
+ for (int i = n / 2; i >= 1; i = i - 1)
+ {
+ downHeap(keys, i, n, lo, comparer);
+ }
+
+ for (int i = n; i > 1; i = i - 1)
+ {
+ swap(keys, lo, lo + i - 1);
+ downHeap(keys, 1, i - 1, lo, comparer);
+ }
+ }
+
+ private static void downHeap(T[] keys, int i, int n, int lo, IComparer comparer)
+ {
+ Contract.Requires(keys != null);
+ Contract.Requires(comparer != null);
+ Contract.Requires(lo >= 0);
+ Contract.Requires(lo < keys.Length);
+
+ T d = keys[lo + i - 1];
+
+ while (i <= n / 2)
+ {
+ var child = 2 * i;
+
+ if (child < n && comparer.Compare(keys[lo + child - 1], keys[lo + child]) < 0)
+ {
+ child++;
+ }
+
+ if (!(comparer.Compare(d, keys[lo + child - 1]) < 0))
+ break;
+
+ keys[lo + i - 1] = keys[lo + child - 1];
+ i = child;
+ }
+
+ keys[lo + i - 1] = d;
+ }
+
+ private static void swap(T[] a, int i, int j)
+ {
+ if (i != j)
+ {
+ T t = a[i];
+ a[i] = a[j];
+ a[j] = t;
+ }
+ }
+
+ private static void swapIfGreater(T[] keys, IComparer comparer, int a, int b)
+ {
+ if (a != b)
+ {
+ if (comparer.Compare(keys[a], keys[b]) > 0)
+ {
+ T key = keys[a];
+ keys[a] = keys[b];
+ keys[b] = key;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
index 13fdd74113..8fd5950dfb 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
@@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Mods
typeof(ManiaModKey7),
typeof(ManiaModKey8),
typeof(ManiaModKey9),
+ typeof(ManiaModKey10),
}.Except(new[] { GetType() }).ToArray();
}
}
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
index fec1360b26..d49ffa01c5 100644
--- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
@@ -10,7 +10,7 @@
["soft-hitnormal"],
["drum-hitnormal"]
],
- "Samples": ["drum-hitnormal"]
+ "Samples": ["-hitnormal"]
}, {
"StartTime": 1875.0,
"EndTime": 2750.0,
@@ -19,7 +19,7 @@
["soft-hitnormal"],
["drum-hitnormal"]
],
- "Samples": ["drum-hitnormal"]
+ "Samples": ["-hitnormal"]
}]
}, {
"StartTime": 3750.0,
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png
new file mode 100644
index 0000000000..c6c3771593
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png
new file mode 100644
index 0000000000..232560a1d4
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs b/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs
new file mode 100644
index 0000000000..23d9d265be
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs
@@ -0,0 +1,41 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.UI;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestPlayfieldBorder : OsuTestScene
+ {
+ public TestPlayfieldBorder()
+ {
+ Bindable playfieldBorderStyle = new Bindable();
+
+ AddStep("add drawables", () =>
+ {
+ Child = new Container
+ {
+ Size = new Vector2(400, 300),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new PlayfieldBorder
+ {
+ PlayfieldBorderStyle = { BindTarget = playfieldBorderStyle }
+ }
+ }
+ };
+ });
+
+ AddStep("Set none", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.None);
+ AddStep("Set corners", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.Corners);
+ AddStep("Set full", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.Full);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
index f76635a932..e8272057f3 100644
--- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
@@ -3,6 +3,7 @@
using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Configuration
{
@@ -19,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
Set(OsuRulesetSetting.SnakingInSliders, true);
Set(OsuRulesetSetting.SnakingOutSliders, true);
Set(OsuRulesetSetting.ShowCursorTrail, true);
+ Set(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
}
}
@@ -26,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
{
SnakingInSliders,
SnakingOutSliders,
- ShowCursorTrail
+ ShowCursorTrail,
+ PlayfieldBorderStyle,
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
index a9879013f8..fff033357d 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
@@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public double SpeedStrain;
public double ApproachRate;
public double OverallDifficulty;
+ public int HitCircleCount;
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index b0d261a1cc..6027635b75 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -47,6 +47,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
maxCombo += beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1);
+ int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
+
return new OsuDifficultyAttributes
{
StarRating = starRating,
@@ -56,6 +58,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6,
MaxCombo = maxCombo,
+ HitCircleCount = hitCirclesCount,
Skills = skills
};
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 02577461f0..063cde8747 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -5,11 +5,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
-using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@@ -19,9 +17,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes;
- private readonly int countHitCircles;
- private readonly int beatmapMaxCombo;
-
private Mod[] mods;
private double accuracy;
@@ -31,14 +26,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private int countMeh;
private int countMiss;
- public OsuPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
- : base(ruleset, beatmap, score)
+ public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
+ : base(ruleset, attributes, score)
{
- countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle);
-
- beatmapMaxCombo = Beatmap.HitObjects.Count;
- // Add the ticks + tail of the slider. 1 is subtracted because the "headcircle" would be counted twice (once for the slider itself in the line above)
- beatmapMaxCombo += Beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1);
}
public override double Calculate(Dictionary categoryRatings = null)
@@ -81,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
categoryRatings.Add("Accuracy", accuracyValue);
categoryRatings.Add("OD", Attributes.OverallDifficulty);
categoryRatings.Add("AR", Attributes.ApproachRate);
- categoryRatings.Add("Max Combo", beatmapMaxCombo);
+ categoryRatings.Add("Max Combo", Attributes.MaxCombo);
}
return totalValue;
@@ -106,8 +96,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= Math.Pow(0.97, countMiss);
// Combo scaling
- if (beatmapMaxCombo > 0)
- aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0);
+ if (Attributes.MaxCombo > 0)
+ aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0;
@@ -154,8 +144,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= Math.Pow(0.97, countMiss);
// Combo scaling
- if (beatmapMaxCombo > 0)
- speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0);
+ if (Attributes.MaxCombo > 0)
+ speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0;
if (Attributes.ApproachRate > 10.33)
@@ -178,7 +168,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window
double betterAccuracyPercentage;
- int amountHitObjectsWithAccuracy = countHitCircles;
+ int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
if (amountHitObjectsWithAccuracy > 0)
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
index 3dbbdcc5d0..e14d6647d2 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
@@ -21,6 +21,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
InternalChild = circlePiece = new HitCirclePiece();
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ BeginPlacement();
+ }
+
protected override void Update()
{
base.Update();
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
index 9349ef7a18..5581ce4bfd 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
@@ -49,6 +49,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
OriginPosition = body.PathOffset;
}
+ public void RecyclePath() => body.RecyclePath();
+
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 94862eb205..d3fb5defae 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -24,10 +24,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public class SliderSelectionBlueprint : OsuSelectionBlueprint
{
- protected readonly SliderBodyPiece BodyPiece;
- protected readonly SliderCircleSelectionBlueprint HeadBlueprint;
- protected readonly SliderCircleSelectionBlueprint TailBlueprint;
- protected readonly PathControlPointVisualiser ControlPointVisualiser;
+ protected SliderBodyPiece BodyPiece { get; private set; }
+ protected SliderCircleSelectionBlueprint HeadBlueprint { get; private set; }
+ protected SliderCircleSelectionBlueprint TailBlueprint { get; private set; }
+ protected PathControlPointVisualiser ControlPointVisualiser { get; private set; }
+
+ private readonly DrawableSlider slider;
[Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; }
@@ -44,17 +46,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
{
- var sliderObject = (Slider)slider.HitObject;
+ this.slider = slider;
+ }
+ [BackgroundDependencyLoader]
+ private void load()
+ {
InternalChildren = new Drawable[]
{
BodyPiece = new SliderBodyPiece(),
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
- ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true)
- {
- RemoveControlPointsRequested = removeControlPoints
- }
};
}
@@ -66,13 +68,35 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
pathVersion = HitObject.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => updatePath());
+
+ BodyPiece.UpdateFrom(HitObject);
}
protected override void Update()
{
base.Update();
- BodyPiece.UpdateFrom(HitObject);
+ if (IsSelected)
+ BodyPiece.UpdateFrom(HitObject);
+ }
+
+ protected override void OnSelected()
+ {
+ AddInternal(ControlPointVisualiser = new PathControlPointVisualiser((Slider)slider.HitObject, true)
+ {
+ RemoveControlPointsRequested = removeControlPoints
+ });
+
+ base.OnSelected();
+ }
+
+ protected override void OnDeselected()
+ {
+ base.OnDeselected();
+
+ // throw away frame buffers on deselection.
+ ControlPointVisualiser?.Expire();
+ BodyPiece.RecyclePath();
}
private Vector2 rightClickPosition;
@@ -182,7 +206,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePath()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
- editorBeatmap?.UpdateHitObject(HitObject);
+ editorBeatmap?.Update(HitObject);
}
public override MenuItem[] ContextMenuItems => new MenuItem[]
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 912a705d16..edd684d886 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -56,7 +56,18 @@ namespace osu.Game.Rulesets.Osu.Edit
[BackgroundDependencyLoader]
private void load()
{
- LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both });
+ LayerBelowRuleset.AddRange(new Drawable[]
+ {
+ new PlayfieldBorder
+ {
+ RelativeSizeAxes = Axes.Both,
+ PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
+ },
+ distanceSnapGridContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ });
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index 9418565907..a72dcff1e9 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -1,7 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -10,40 +17,268 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuSelectionHandler : SelectionHandler
{
- public override bool HandleMovement(MoveSelectionEvent moveEvent)
+ protected override void OnSelectionChanged()
{
- Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
- Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
+ base.OnSelectionChanged();
- // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
- foreach (var h in SelectedHitObjects.OfType())
+ bool canOperate = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
+
+ SelectionBox.CanRotate = canOperate;
+ SelectionBox.CanScaleX = canOperate;
+ SelectionBox.CanScaleY = canOperate;
+ SelectionBox.CanReverse = canOperate;
+ }
+
+ protected override void OnOperationEnded()
+ {
+ base.OnOperationEnded();
+ referenceOrigin = null;
+ }
+
+ public override bool HandleMovement(MoveSelectionEvent moveEvent) =>
+ moveSelection(moveEvent.InstantDelta);
+
+ ///
+ /// During a transform, the initial origin is stored so it can be used throughout the operation.
+ ///
+ private Vector2? referenceOrigin;
+
+ public override bool HandleReverse()
+ {
+ var hitObjects = selectedMovableObjects;
+
+ double endTime = hitObjects.Max(h => h.GetEndTime());
+ double startTime = hitObjects.Min(h => h.StartTime);
+
+ bool moreThanOneObject = hitObjects.Length > 1;
+
+ foreach (var h in hitObjects)
{
- if (h is Spinner)
+ if (moreThanOneObject)
+ h.StartTime = endTime - (h.GetEndTime() - startTime);
+
+ if (h is Slider slider)
{
- // Spinners don't support position adjustments
- continue;
+ var points = slider.Path.ControlPoints.ToArray();
+ Vector2 endPos = points.Last().Position.Value;
+
+ slider.Path.ControlPoints.Clear();
+
+ slider.Position += endPos;
+
+ PathType? lastType = null;
+
+ for (var i = 0; i < points.Length; i++)
+ {
+ var p = points[i];
+ p.Position.Value -= endPos;
+
+ // propagate types forwards to last null type
+ if (i == points.Length - 1)
+ p.Type.Value = lastType;
+ else if (p.Type.Value != null)
+ {
+ var newType = p.Type.Value;
+ p.Type.Value = lastType;
+ lastType = newType;
+ }
+
+ slider.Path.ControlPoints.Insert(0, p);
+ }
}
-
- // Stacking is not considered
- minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
- maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
- }
-
- if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight)
- return false;
-
- foreach (var h in SelectedHitObjects.OfType())
- {
- if (h is Spinner)
- {
- // Spinners don't support position adjustments
- continue;
- }
-
- h.Position += moveEvent.InstantDelta;
}
return true;
}
+
+ public override bool HandleFlip(Direction direction)
+ {
+ var hitObjects = selectedMovableObjects;
+
+ var selectedObjectsQuad = getSurroundingQuad(hitObjects);
+ var centre = selectedObjectsQuad.Centre;
+
+ foreach (var h in hitObjects)
+ {
+ var pos = h.Position;
+
+ switch (direction)
+ {
+ case Direction.Horizontal:
+ pos.X = centre.X - (pos.X - centre.X);
+ break;
+
+ case Direction.Vertical:
+ pos.Y = centre.Y - (pos.Y - centre.Y);
+ break;
+ }
+
+ h.Position = pos;
+
+ if (h is Slider slider)
+ {
+ foreach (var point in slider.Path.ControlPoints)
+ {
+ point.Position.Value = new Vector2(
+ (direction == Direction.Horizontal ? -1 : 1) * point.Position.Value.X,
+ (direction == Direction.Vertical ? -1 : 1) * point.Position.Value.Y
+ );
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public override bool HandleScale(Vector2 scale, Anchor reference)
+ {
+ adjustScaleFromAnchor(ref scale, reference);
+
+ var hitObjects = selectedMovableObjects;
+
+ // for the time being, allow resizing of slider paths only if the slider is
+ // the only hit object selected. with a group selection, it's likely the user
+ // is not looking to change the duration of the slider but expand the whole pattern.
+ if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
+ {
+ Quad quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
+ Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / quad.Width, 1 + scale.Y / quad.Height);
+
+ foreach (var point in slider.Path.ControlPoints)
+ point.Position.Value *= pathRelativeDeltaScale;
+ }
+ else
+ {
+ // move the selection before scaling if dragging from top or left anchors.
+ if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false;
+ if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false;
+
+ Quad quad = getSurroundingQuad(hitObjects);
+
+ foreach (var h in hitObjects)
+ {
+ h.Position = new Vector2(
+ quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X),
+ quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y)
+ );
+ }
+ }
+
+ return true;
+ }
+
+ private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
+ {
+ // cancel out scale in axes we don't care about (based on which drag handle was used).
+ if ((reference & Anchor.x1) > 0) scale.X = 0;
+ if ((reference & Anchor.y1) > 0) scale.Y = 0;
+
+ // reverse the scale direction if dragging from top or left.
+ if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
+ if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
+ }
+
+ public override bool HandleRotation(float delta)
+ {
+ var hitObjects = selectedMovableObjects;
+
+ Quad quad = getSurroundingQuad(hitObjects);
+
+ referenceOrigin ??= quad.Centre;
+
+ foreach (var h in hitObjects)
+ {
+ h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
+
+ if (h is IHasPath path)
+ {
+ foreach (var point in path.Path.ControlPoints)
+ point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
+ }
+ }
+
+ // this isn't always the case but let's be lenient for now.
+ return true;
+ }
+
+ private bool moveSelection(Vector2 delta)
+ {
+ var hitObjects = selectedMovableObjects;
+
+ Quad quad = getSurroundingQuad(hitObjects);
+
+ if (quad.TopLeft.X + delta.X < 0 ||
+ quad.TopLeft.Y + delta.Y < 0 ||
+ quad.BottomRight.X + delta.X > DrawWidth ||
+ quad.BottomRight.Y + delta.Y > DrawHeight)
+ return false;
+
+ foreach (var h in hitObjects)
+ h.Position += delta;
+
+ return true;
+ }
+
+ ///
+ /// Returns a gamefield-space quad surrounding the provided hit objects.
+ ///
+ /// The hit objects to calculate a quad for.
+ private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
+ getSurroundingQuad(hitObjects.SelectMany(h => new[] { h.Position, h.EndPosition }));
+
+ ///
+ /// Returns a gamefield-space quad surrounding the provided points.
+ ///
+ /// The points to calculate a quad for.
+ private Quad getSurroundingQuad(IEnumerable points)
+ {
+ if (!EditorBeatmap.SelectedHitObjects.Any())
+ return new Quad();
+
+ Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
+ Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
+
+ // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
+ foreach (var p in points)
+ {
+ minPosition = Vector2.ComponentMin(minPosition, p);
+ maxPosition = Vector2.ComponentMax(maxPosition, p);
+ }
+
+ Vector2 size = maxPosition - minPosition;
+
+ return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
+ }
+
+ ///
+ /// All osu! hitobjects which can be moved/rotated/scaled.
+ ///
+ private OsuHitObject[] selectedMovableObjects => EditorBeatmap.SelectedHitObjects
+ .OfType()
+ .Where(h => !(h is Spinner))
+ .ToArray();
+
+ ///
+ /// Rotate a point around an arbitrary origin.
+ ///
+ /// The point.
+ /// The centre origin to rotate around.
+ /// The angle to rotate (in degrees).
+ private static Vector2 rotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
+ {
+ angle = -angle;
+
+ point.X -= origin.X;
+ point.Y -= origin.Y;
+
+ Vector2 ret;
+ ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
+ ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
+
+ ret.X += origin.X;
+ ret.Y += origin.Y;
+
+ return ret;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index 2263e2b2f4..8c819c4773 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -11,7 +11,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.UI;
-using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -31,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private OsuInputManager inputManager;
- private GameplayClock gameplayClock;
+ private IFrameStableClock gameplayClock;
private List replayFrames;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 08fd13915d..f69cacd432 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -39,7 +39,14 @@ namespace osu.Game.Rulesets.Osu.Mods
base.ApplyToDrawableHitObjects(drawables);
}
- protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state)
+ private double lastSliderHeadFadeOutStartTime;
+ private double lastSliderHeadFadeOutDuration;
+
+ protected override void ApplyFirstObjectIncreaseVisibilityState(DrawableHitObject drawable, ArmedState state) => applyState(drawable, true);
+
+ protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) => applyState(drawable, false);
+
+ private void applyState(DrawableHitObject drawable, bool increaseVisibility)
{
if (!(drawable is DrawableOsuHitObject d))
return;
@@ -54,15 +61,52 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable)
{
+ case DrawableSliderTail sliderTail:
+ // use stored values from head circle to achieve same fade sequence.
+ fadeOutDuration = lastSliderHeadFadeOutDuration;
+ fadeOutStartTime = lastSliderHeadFadeOutStartTime;
+
+ using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
+ sliderTail.FadeOut(fadeOutDuration);
+
+ break;
+
+ case DrawableSliderRepeat sliderRepeat:
+ // use stored values from head circle to achieve same fade sequence.
+ fadeOutDuration = lastSliderHeadFadeOutDuration;
+ fadeOutStartTime = lastSliderHeadFadeOutStartTime;
+
+ using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
+ // only apply to circle piece – reverse arrow is not affected by hidden.
+ sliderRepeat.CirclePiece.FadeOut(fadeOutDuration);
+
+ break;
+
case DrawableHitCircle circle:
- // we don't want to see the approach circle
- using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
- circle.ApproachCircle.Hide();
+
+ if (circle is DrawableSliderHead)
+ {
+ lastSliderHeadFadeOutDuration = fadeOutDuration;
+ lastSliderHeadFadeOutStartTime = fadeOutStartTime;
+ }
+
+ Drawable fadeTarget = circle;
+
+ if (increaseVisibility)
+ {
+ // only fade the circle piece (not the approach circle) for the increased visibility object.
+ fadeTarget = circle.CirclePiece;
+ }
+ else
+ {
+ // we don't want to see the approach circle
+ using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
+ circle.ApproachCircle.Hide();
+ }
// fade out immediately after fade in.
using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
- circle.FadeOut(fadeOutDuration);
-
+ fadeTarget.FadeOut(fadeOutDuration);
break;
case DrawableSlider slider:
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
index d7582f3196..bb2213aa31 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
@@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Bindables;
using System.Collections.Generic;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
@@ -38,20 +39,25 @@ namespace osu.Game.Rulesets.Osu.Mods
protected void ApplyTraceableState(DrawableHitObject drawable, ArmedState state)
{
- if (!(drawable is DrawableOsuHitObject drawableOsu))
+ if (!(drawable is DrawableOsuHitObject))
return;
- var h = drawableOsu.HitObject;
-
//todo: expose and hide spinner background somehow
switch (drawable)
{
case DrawableHitCircle circle:
// we only want to see the approach circle
- using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
- circle.CirclePiece.Hide();
+ applyCirclePieceState(circle, circle.CirclePiece);
+ break;
+ case DrawableSliderTail sliderTail:
+ applyCirclePieceState(sliderTail);
+ break;
+
+ case DrawableSliderRepeat sliderRepeat:
+ // show only the repeat arrow
+ applyCirclePieceState(sliderRepeat, sliderRepeat.CirclePiece);
break;
case DrawableSlider slider:
@@ -61,6 +67,13 @@ namespace osu.Game.Rulesets.Osu.Mods
}
}
+ private void applyCirclePieceState(DrawableOsuHitObject hitObject, IDrawable hitCircle = null)
+ {
+ var h = hitObject.HitObject;
+ using (hitObject.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
+ (hitCircle ?? hitObject).Hide();
+ }
+
private void applySliderState(DrawableSlider slider)
{
((PlaySliderBody)slider.Body.Drawable).AccentColour = slider.AccentColour.Value.Opacity(0);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 280ca33234..b00d12983d 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -51,6 +51,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
InternalChildren = new Drawable[]
{
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
+ tailContainer = new Container { RelativeSizeAxes = Axes.Both },
tickContainer = new Container { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container { RelativeSizeAxes = Axes.Both },
Ball = new SliderBall(s, this)
@@ -62,7 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Alpha = 0
},
headContainer = new Container { RelativeSizeAxes = Axes.Both },
- tailContainer = new Container { RelativeSizeAxes = Axes.Both },
};
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index f65077685f..2a88f11f69 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
@@ -6,9 +6,11 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
@@ -22,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Drawable scaleContainer;
+ public readonly Drawable CirclePiece;
+
public override bool DisplayResult => false;
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
@@ -34,7 +38,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
- InternalChild = scaleContainer = new ReverseArrowPiece();
+ InternalChild = scaleContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new[]
+ {
+ // no default for this; only visible in legacy skins.
+ CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()),
+ arrow = new ReverseArrowPiece(),
+ }
+ };
}
private readonly IBindable scaleBindable = new BindableFloat();
@@ -85,6 +100,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private bool hasRotation;
+ private readonly ReverseArrowPiece arrow;
+
public void UpdateSnakingPosition(Vector2 start, Vector2 end)
{
// When the repeat is hit, the arrow should fade out on spot rather than following the slider
@@ -114,18 +131,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
- while (Math.Abs(aimRotation - Rotation) > 180)
- aimRotation += aimRotation < Rotation ? 360 : -360;
+ while (Math.Abs(aimRotation - arrow.Rotation) > 180)
+ aimRotation += aimRotation < arrow.Rotation ? 360 : -360;
if (!hasRotation)
{
- Rotation = aimRotation;
+ arrow.Rotation = aimRotation;
hasRotation = true;
}
else
{
// If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly).
- Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint);
+ arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
index 0939e2847a..f5bcecccdf 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
@@ -1,15 +1,20 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Diagnostics;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
- public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking
+ public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking
{
- private readonly Slider slider;
+ private readonly SliderTailCircle tailCircle;
///
/// The judgement text is provided by the .
@@ -18,28 +23,73 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public bool Tracking { get; set; }
- private readonly IBindable positionBindable = new Bindable();
- private readonly IBindable pathVersion = new Bindable();
+ private readonly IBindable scaleBindable = new BindableFloat();
- public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle)
- : base(hitCircle)
+ private readonly SkinnableDrawable circlePiece;
+
+ private readonly Container scaleContainer;
+
+ public DrawableSliderTail(Slider slider, SliderTailCircle tailCircle)
+ : base(tailCircle)
{
- this.slider = slider;
-
+ this.tailCircle = tailCircle;
Origin = Anchor.Centre;
- RelativeSizeAxes = Axes.Both;
- FillMode = FillMode.Fit;
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
- AlwaysPresent = true;
+ InternalChildren = new Drawable[]
+ {
+ scaleContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ // no default for this; only visible in legacy skins.
+ circlePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
+ }
+ },
+ };
+ }
- positionBindable.BindTo(hitCircle.PositionBindable);
- pathVersion.BindTo(slider.Path.Version);
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true);
+ scaleBindable.BindTo(HitObject.ScaleBindable);
+ }
- positionBindable.BindValueChanged(_ => updatePosition());
- pathVersion.BindValueChanged(_ => updatePosition(), true);
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
- // TODO: This has no drawable content. Support for skins should be added.
+ circlePiece.FadeInFromZero(HitObject.TimeFadeIn);
+ }
+
+ protected override void UpdateStateTransforms(ArmedState state)
+ {
+ base.UpdateStateTransforms(state);
+
+ Debug.Assert(HitObject.HitWindows != null);
+
+ switch (state)
+ {
+ case ArmedState.Idle:
+ this.Delay(HitObject.TimePreempt).FadeOut(500);
+
+ Expire(true);
+ break;
+
+ case ArmedState.Miss:
+ this.FadeOut(100);
+ break;
+
+ case ArmedState.Hit:
+ // todo: temporary / arbitrary
+ this.Delay(800).FadeOut();
+ break;
+ }
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
@@ -48,6 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
- private void updatePosition() => Position = HitObject.Position - slider.Position;
+ public void UpdateSnakingPosition(Vector2 start, Vector2 end) =>
+ Position = tailCircle.RepeatIndex % 2 == 0 ? end : start;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 130b4e6e53..936bfaeb86 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return;
// Trigger a miss result for remaining ticks to avoid infinite gameplay.
- foreach (var tick in ticks.Where(t => !t.IsHit))
+ foreach (var tick in ticks.Where(t => !t.Result.HasResult))
tick.TriggerResult(false);
ApplyResult(r =>
@@ -268,7 +268,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
while (wholeSpins != spins)
{
- var tick = ticks.FirstOrDefault(t => !t.IsHit);
+ var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
// tick may be null if we've hit the spin limit.
if (tick != null)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs
index bcf64b81a6..619fea73bc 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Origin = Anchor.Centre;
Masking = true;
- BorderThickness = 10;
+ BorderThickness = 9; // roughly matches slider borders and makes stacked circles distinctly visible from each other.
BorderColour = Color4.White;
Child = new Box
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 51f6a44a87..755ce0866a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -137,6 +137,10 @@ namespace osu.Game.Rulesets.Osu.Objects
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
+
+ // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
+ // For now, the samples are attached to and played by the slider itself at the correct end time.
+ Samples = this.GetNodeSamples(repeatCount + 1);
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
@@ -176,6 +180,7 @@ namespace osu.Game.Rulesets.Osu.Objects
// if this is to change, we should revisit this.
AddNested(TailCircle = new SliderTailCircle(this)
{
+ RepeatIndex = e.SpanIndex,
StartTime = e.Time,
Position = EndPosition,
StackHeight = StackHeight
@@ -183,10 +188,9 @@ namespace osu.Game.Rulesets.Osu.Objects
break;
case SliderEventType.Repeat:
- AddNested(new SliderRepeat
+ AddNested(new SliderRepeat(this)
{
RepeatIndex = e.SpanIndex,
- SpanDuration = SpanDuration,
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
@@ -230,15 +234,12 @@ namespace osu.Game.Rulesets.Osu.Objects
tick.Samples = sampleList;
foreach (var repeat in NestedHitObjects.OfType())
- repeat.Samples = getNodeSamples(repeat.RepeatIndex + 1);
+ repeat.Samples = this.GetNodeSamples(repeat.RepeatIndex + 1);
if (HeadCircle != null)
- HeadCircle.Samples = getNodeSamples(0);
+ HeadCircle.Samples = this.GetNodeSamples(0);
}
- private IList getNodeSamples(int nodeIndex) =>
- nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
-
public override Judgement CreateJudgement() => new OsuIgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs
new file mode 100644
index 0000000000..a6aed2c00e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Objects
+{
+ ///
+ /// A hit circle which is at the end of a slider path (either repeat or final tail).
+ ///
+ public abstract class SliderEndCircle : HitCircle
+ {
+ private readonly Slider slider;
+
+ protected SliderEndCircle(Slider slider)
+ {
+ this.slider = slider;
+ }
+
+ public int RepeatIndex { get; set; }
+
+ public double SpanDuration => slider.SpanDuration;
+
+ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ {
+ base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+
+ if (RepeatIndex > 0)
+ {
+ // Repeat points after the first span should appear behind the still-visible one.
+ TimeFadeIn = 0;
+
+ // The next end circle should appear exactly after the previous circle (on the same end) is hit.
+ TimePreempt = SpanDuration * 2;
+ }
+ else
+ {
+ // taken from osu-stable
+ const float first_end_circle_preempt_adjust = 2 / 3f;
+
+ // The first end circle should fade in with the slider.
+ TimePreempt = (StartTime - slider.StartTime) + slider.TimePreempt * first_end_circle_preempt_adjust;
+ }
+ }
+
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs
index b6c58a75d1..cca86361c2 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs
@@ -1,35 +1,19 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
- public class SliderRepeat : OsuHitObject
+ public class SliderRepeat : SliderEndCircle
{
- public int RepeatIndex { get; set; }
- public double SpanDuration { get; set; }
-
- protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ public SliderRepeat(Slider slider)
+ : base(slider)
{
- base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
-
- // Out preempt should be one span early to give the user ample warning.
- TimePreempt += SpanDuration;
-
- // We want to show the first RepeatPoint as the TimePreempt dictates but on short (and possibly fast) sliders
- // we may need to cut down this time on following RepeatPoints to only show up to two RepeatPoints at any given time.
- if (RepeatIndex > 0)
- TimePreempt = Math.Min(SpanDuration * 2, TimePreempt);
}
- protected override HitWindows CreateHitWindows() => HitWindows.Empty;
-
public override Judgement CreateJudgement() => new SliderRepeatJudgement();
public class SliderRepeatJudgement : OsuJudgement
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
index 3afd36669f..f9450062f4 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
@@ -13,18 +12,13 @@ namespace osu.Game.Rulesets.Osu.Objects
/// Note that this should not be used for timing correctness.
/// See usage in for more information.
///
- public class SliderTailCircle : SliderCircle
+ public class SliderTailCircle : SliderEndCircle
{
- private readonly IBindable pathVersion = new Bindable();
-
public SliderTailCircle(Slider slider)
+ : base(slider)
{
- pathVersion.BindTo(slider.Path.Version);
- pathVersion.BindValueChanged(_ => Position = slider.EndPosition);
}
- protected override HitWindows CreateHitWindows() => HitWindows.Empty;
-
public override Judgement CreateJudgement() => new SliderTailJudgement();
public class SliderTailJudgement : OsuJudgement
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 7f4a0dcbbb..678fb8aba6 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(this, beatmap);
- public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new OsuPerformanceCalculator(this, beatmap, score);
+ public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new OsuPerformanceCalculator(this, attributes, score);
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
@@ -191,6 +191,41 @@ namespace osu.Game.Rulesets.Osu
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
+ protected override IEnumerable GetValidHitResults()
+ {
+ return new[]
+ {
+ HitResult.Great,
+ HitResult.Ok,
+ HitResult.Meh,
+
+ HitResult.LargeTickHit,
+ HitResult.SmallTickHit,
+ HitResult.SmallBonus,
+ HitResult.LargeBonus,
+ };
+ }
+
+ public override string GetDisplayNameForHitResult(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.LargeTickHit:
+ return "slider tick";
+
+ case HitResult.SmallTickHit:
+ return "slider end";
+
+ case HitResult.SmallBonus:
+ return "spinner spin";
+
+ case HitResult.LargeBonus:
+ return "spinner bonus";
+ }
+
+ return base.GetDisplayNameForHitResult(result);
+ }
+
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index 5468764692..2883f0c187 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu
ReverseArrow,
HitCircleText,
SliderHeadHitCircle,
+ SliderTailHitCircle,
SliderFollowCircle,
SliderBall,
SliderBody,
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs
index 1885c76fcc..e6cd7bc59d 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs
@@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Skinning;
@@ -15,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
private bool disjointTrail;
private double lastTrailTime;
+ private IBindable cursorSize;
public LegacyCursorTrail()
{
@@ -22,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
[BackgroundDependencyLoader]
- private void load(ISkinSource skin)
+ private void load(ISkinSource skin, OsuConfigManager config)
{
Texture = skin.GetTexture("cursortrail");
disjointTrail = skin.GetTexture("cursormiddle") == null;
@@ -32,12 +36,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
// stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
Texture.ScaleAdjust *= 1.6f;
}
+
+ cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize).GetBoundCopy();
}
protected override double FadeDuration => disjointTrail ? 150 : 500;
protected override bool InterpolateMovements => !disjointTrail;
+ protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
+
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (!disjointTrail)
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
index d15a0a3203..382d6e53cc 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
@@ -21,10 +21,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
public class LegacyMainCirclePiece : CompositeDrawable
{
private readonly string priorityLookup;
+ private readonly bool hasNumber;
- public LegacyMainCirclePiece(string priorityLookup = null)
+ public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true)
{
this.priorityLookup = priorityLookup;
+ this.hasNumber = hasNumber;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
}
@@ -47,6 +49,23 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject;
+ bool allowFallback = false;
+
+ // attempt lookup using priority specification
+ Texture baseTexture = getTextureWithFallback(string.Empty);
+
+ // if the base texture was not found without a fallback, switch on fallback mode and re-perform the lookup.
+ if (baseTexture == null)
+ {
+ allowFallback = true;
+ baseTexture = getTextureWithFallback(string.Empty);
+ }
+
+ // at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
+ // the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
+ // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin).
+ Texture overlayTexture = getTextureWithFallback("overlay");
+
InternalChildren = new Drawable[]
{
circleSprites = new Container
@@ -58,19 +77,23 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
hitCircleSprite = new Sprite
{
- Texture = getTextureWithFallback(string.Empty),
+ Texture = baseTexture,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
hitCircleOverlay = new Sprite
{
- Texture = getTextureWithFallback("overlay"),
+ Texture = overlayTexture,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
},
- hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
+ };
+
+ if (hasNumber)
+ {
+ AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{
Font = OsuFont.Numeric.With(size: 40),
UseFullGlyphHeight = false,
@@ -78,8 +101,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- },
- };
+ });
+ }
bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
@@ -95,8 +118,13 @@ namespace osu.Game.Rulesets.Osu.Skinning
Texture tex = null;
if (!string.IsNullOrEmpty(priorityLookup))
+ {
tex = skin.GetTexture($"{priorityLookup}{name}");
+ if (!allowFallback)
+ return tex;
+ }
+
return tex ?? skin.GetTexture($"hitcircle{name}");
}
}
@@ -107,7 +135,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
- indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
+ if (hasNumber)
+ indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
}
private void updateState(ValueChangedEvent state)
@@ -120,16 +149,19 @@ namespace osu.Game.Rulesets.Osu.Skinning
circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
- var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value;
-
- if (legacyVersion >= 2.0m)
- // legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
- hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
- else
+ if (hasNumber)
{
- // old skins scale and fade it normally along other pieces.
- hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
- hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value;
+
+ if (legacyVersion >= 2.0m)
+ // legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
+ hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
+ else
+ {
+ // old skins scale and fade it normally along other pieces.
+ hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
+ hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ }
}
break;
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
index 851a8d56c9..78bc26eff7 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
@@ -66,6 +66,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
return null;
+ case OsuSkinComponents.SliderTailHitCircle:
+ if (hasHitCircle.Value)
+ return new LegacyMainCirclePiece("sliderendcircle", false);
+
+ return null;
+
case OsuSkinComponents.SliderHeadHitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece("sliderstartcircle");
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index 9bcb3abc63..0b30c28b8d 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -119,6 +119,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
///
protected virtual bool InterpolateMovements => true;
+ protected virtual float IntervalMultiplier => 1.0f;
+
private Vector2? lastPosition;
private readonly InputResampler resampler = new InputResampler();
@@ -147,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
float distance = diff.Length;
Vector2 direction = diff / distance;
- float interval = partSize.X / 2.5f;
+ float interval = partSize.X / 2.5f * IntervalMultiplier;
for (float d = interval; d < distance; d += interval)
{
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 4ef9bbe091..50727d590a 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -17,12 +17,16 @@ using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Game.Rulesets.Osu.Configuration;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI
{
public class OsuPlayfield : Playfield
{
+ private readonly PlayfieldBorder playfieldBorder;
private readonly ProxyContainer approachCircles;
private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer judgementLayer;
@@ -33,12 +37,19 @@ namespace osu.Game.Rulesets.Osu.UI
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
+ private readonly Bindable playfieldBorderStyle = new BindableBool();
+
private readonly IDictionary> poolDictionary = new Dictionary>();
public OsuPlayfield()
{
InternalChildren = new Drawable[]
{
+ playfieldBorder = new PlayfieldBorder
+ {
+ RelativeSizeAxes = Axes.Both,
+ Depth = 3
+ },
spinnerProxies = new ProxyContainer
{
RelativeSizeAxes = Axes.Both
@@ -76,6 +87,12 @@ namespace osu.Game.Rulesets.Osu.UI
AddRangeInternal(poolDictionary.Values);
}
+ [BackgroundDependencyLoader(true)]
+ private void load(OsuRulesetConfigManager config)
+ {
+ config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
+ }
+
public override void Add(DrawableHitObject h)
{
h.OnNewResult += onNewResult;
diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
index 88adf72551..705ba3e929 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Osu.Configuration;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
@@ -27,17 +28,22 @@ namespace osu.Game.Rulesets.Osu.UI
new SettingsCheckbox
{
LabelText = "Snaking in sliders",
- Bindable = config.GetBindable(OsuRulesetSetting.SnakingInSliders)
+ Current = config.GetBindable(OsuRulesetSetting.SnakingInSliders)
},
new SettingsCheckbox
{
LabelText = "Snaking out sliders",
- Bindable = config.GetBindable(OsuRulesetSetting.SnakingOutSliders)
+ Current = config.GetBindable(OsuRulesetSetting.SnakingOutSliders)
},
new SettingsCheckbox
{
LabelText = "Cursor trail",
- Bindable = config.GetBindable(OsuRulesetSetting.ShowCursorTrail)
+ Current = config.GetBindable(OsuRulesetSetting.ShowCursorTrail)
+ },
+ new SettingsEnumDropdown
+ {
+ LabelText = "Playfield border style",
+ Current = config.GetBindable(OsuRulesetSetting.PlayfieldBorderStyle),
},
};
}
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index ed7b8589ba..607eaf5dbd 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>
{
TaikoHitObject first = x.First();
- if (x.Skip(1).Any() && !(first is Swell))
+ if (x.Skip(1).Any() && first.CanBeStrong)
first.IsStrong = true;
return first;
}).ToList();
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index c04fffa2e7..2d9b95ae88 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
-using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@@ -24,8 +23,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private int countMeh;
private int countMiss;
- public TaikoPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
- : base(ruleset, beatmap, score)
+ public TaikoPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
+ : base(ruleset, attributes, score)
{
}
diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
index d5dd758e10..a05de1f217 100644
--- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
@@ -52,32 +52,32 @@ namespace osu.Game.Rulesets.Taiko.Edit
public void SetStrongState(bool state)
{
- var hits = SelectedHitObjects.OfType();
+ var hits = EditorBeatmap.SelectedHitObjects.OfType();
- ChangeHandler.BeginChange();
+ EditorBeatmap.BeginChange();
foreach (var h in hits)
{
if (h.IsStrong != state)
{
h.IsStrong = state;
- EditorBeatmap.UpdateHitObject(h);
+ EditorBeatmap.Update(h);
}
}
- ChangeHandler.EndChange();
+ EditorBeatmap.EndChange();
}
public void SetRimState(bool state)
{
- var hits = SelectedHitObjects.OfType();
+ var hits = EditorBeatmap.SelectedHitObjects.OfType();
- ChangeHandler.BeginChange();
+ EditorBeatmap.BeginChange();
foreach (var h in hits)
h.Type = state ? HitType.Rim : HitType.Centre;
- ChangeHandler.EndChange();
+ EditorBeatmap.EndChange();
}
protected override IEnumerable