Merge branch 'master' into fix_progress_bar_info

This commit is contained in:
Dean Herbert
2022-08-26 16:14:21 +09:00
412 changed files with 6091 additions and 2767 deletions

View File

@ -21,7 +21,7 @@
] ]
}, },
"ppy.localisationanalyser.tools": { "ppy.localisationanalyser.tools": {
"version": "2022.607.0", "version": "2022.809.0",
"commands": [ "commands": [
"localisation" "localisation"
] ]

View File

@ -53,3 +53,7 @@ dotnet_diagnostic.CA2225.severity = none
# Banned APIs # Banned APIs
dotnet_diagnostic.RS0030.severity = error dotnet_diagnostic.RS0030.severity = error
# Temporarily disable analysing CanBeNull = true in NRT contexts due to mobile issues.
# See: https://github.com/ppy/osu/pull/19677
dotnet_diagnostic.OSUF001.severity = none

View File

@ -4,8 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
@ -50,9 +48,6 @@ namespace osu.Game.Rulesets.Pippidon
new KeyBinding(InputKey.X, PippidonAction.Button2), new KeyBinding(InputKey.X, PippidonAction.Button2),
}; };
public override Drawable CreateIcon() => new Sprite public override Drawable CreateIcon() => new PippidonRulesetIcon(this);
{
Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"),
};
} }
} }

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
namespace osu.Game.Rulesets.Pippidon
{
public class PippidonRulesetIcon : Sprite
{
private readonly Ruleset ruleset;
public PippidonRulesetIcon(Ruleset ruleset)
{
this.ruleset = ruleset;
}
[BackgroundDependencyLoader]
private void load(IRenderer renderer)
{
Texture = new TextureStore(renderer, new TextureLoaderStore(ruleset.CreateResourceStore()), false).Get("Textures/coin");
}
}
}

View File

@ -21,8 +21,11 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps
public PippidonBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) public PippidonBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
: base(beatmap, ruleset) : base(beatmap, ruleset)
{ {
minPosition = beatmap.HitObjects.Min(getUsablePosition); if (beatmap.HitObjects.Any())
maxPosition = beatmap.HitObjects.Max(getUsablePosition); {
minPosition = beatmap.HitObjects.Min(getUsablePosition);
maxPosition = beatmap.HitObjects.Max(getUsablePosition);
}
} }
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition && h is IHasYPosition); public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition && h is IHasYPosition);

View File

@ -4,8 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
@ -47,10 +45,6 @@ namespace osu.Game.Rulesets.Pippidon
new KeyBinding(InputKey.S, PippidonAction.MoveDown), new KeyBinding(InputKey.S, PippidonAction.MoveDown),
}; };
public override Drawable CreateIcon() => new Sprite public override Drawable CreateIcon() => new PippidonRulesetIcon(this);
{
Margin = new MarginPadding { Top = 3 },
Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"),
};
} }
} }

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
namespace osu.Game.Rulesets.Pippidon
{
public class PippidonRulesetIcon : Sprite
{
private readonly Ruleset ruleset;
public PippidonRulesetIcon(Ruleset ruleset)
{
this.ruleset = ruleset;
Margin = new MarginPadding { Top = 3 };
}
[BackgroundDependencyLoader]
private void load(IRenderer renderer)
{
Texture = new TextureStore(renderer, new TextureLoaderStore(ruleset.CreateResourceStore()), false).Get("Textures/coin");
}
}
}

View File

@ -51,11 +51,11 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.722.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.819.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.730.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.819.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
<PackageReference Include="Realm" Version="10.14.0" /> <PackageReference Include="Realm" Version="10.15.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Text; using System.Text;
using DiscordRPC; using DiscordRPC;
@ -26,15 +24,15 @@ namespace osu.Desktop
{ {
private const string client_id = "367827983903490050"; private const string client_id = "367827983903490050";
private DiscordRpcClient client; private DiscordRpcClient client = null!;
[Resolved] [Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } private IBindable<RulesetInfo> ruleset { get; set; } = null!;
private IBindable<APIUser> user; private IBindable<APIUser> user = null!;
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; } = null!;
private readonly IBindable<UserStatus> status = new Bindable<UserStatus>(); private readonly IBindable<UserStatus> status = new Bindable<UserStatus>();
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>(); private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
@ -130,7 +128,7 @@ namespace osu.Desktop
presence.Assets.LargeImageText = string.Empty; presence.Assets.LargeImageText = string.Empty;
else else
{ {
if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics statistics)) if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics? statistics))
presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty); presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty);
else else
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty); presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
@ -164,7 +162,7 @@ namespace osu.Desktop
}); });
} }
private IBeatmapInfo getBeatmap(UserActivity activity) private IBeatmapInfo? getBeatmap(UserActivity activity)
{ {
switch (activity) switch (activity)
{ {
@ -183,10 +181,10 @@ namespace osu.Desktop
switch (activity) switch (activity)
{ {
case UserActivity.InGame game: case UserActivity.InGame game:
return game.BeatmapInfo.ToString(); return game.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.Editing edit: case UserActivity.Editing edit:
return edit.BeatmapInfo.ToString(); return edit.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.InLobby lobby: case UserActivity.InLobby lobby:
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Desktop.LegacyIpc namespace osu.Desktop.LegacyIpc
{ {
/// <summary> /// <summary>
@ -13,7 +11,7 @@ namespace osu.Desktop.LegacyIpc
/// </remarks> /// </remarks>
public class LegacyIpcDifficultyCalculationRequest public class LegacyIpcDifficultyCalculationRequest
{ {
public string BeatmapFile { get; set; } public string BeatmapFile { get; set; } = string.Empty;
public int RulesetId { get; set; } public int RulesetId { get; set; }
public int Mods { get; set; } public int Mods { get; set; }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Desktop.LegacyIpc namespace osu.Desktop.LegacyIpc
{ {
/// <summary> /// <summary>

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Platform; using osu.Framework.Platform;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -39,17 +37,20 @@ namespace osu.Desktop.LegacyIpc
public new object Value public new object Value
{ {
get => base.Value; get => base.Value;
set => base.Value = new Data set => base.Value = new Data(value.GetType().Name, value);
{
MessageType = value.GetType().Name,
MessageData = value
};
} }
public class Data public class Data
{ {
public string MessageType { get; set; } public string MessageType { get; }
public object MessageData { get; set; }
public object MessageData { get; }
public Data(string messageType, object messageData)
{
MessageType = messageType;
MessageData = messageData;
}
} }
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.IO; using System.IO;
using System.Runtime.Versioning; using System.Runtime.Versioning;
@ -27,7 +25,7 @@ namespace osu.Desktop
private const string base_game_name = @"osu"; private const string base_game_name = @"osu";
#endif #endif
private static LegacyTcpIpcProvider legacyIpc; private static LegacyTcpIpcProvider? legacyIpc;
[STAThread] [STAThread]
public static void Main(string[] args) public static void Main(string[] args)

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Security.Principal; using System.Security.Principal;
using osu.Framework; using osu.Framework;
@ -21,7 +19,7 @@ namespace osu.Desktop.Security
public class ElevatedPrivilegesChecker : Component public class ElevatedPrivilegesChecker : Component
{ {
[Resolved] [Resolved]
private INotificationOverlay notifications { get; set; } private INotificationOverlay notifications { get; set; } = null!;
private bool elevated; private bool elevated;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -26,8 +24,8 @@ namespace osu.Desktop.Updater
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
{ {
private UpdateManager updateManager; private UpdateManager? updateManager;
private INotificationOverlay notificationOverlay; private INotificationOverlay notificationOverlay = null!;
public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited(); public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited();
@ -50,12 +48,12 @@ namespace osu.Desktop.Updater
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false); protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
private async Task<bool> checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null) private async Task<bool> checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification? notification = null)
{ {
// should we schedule a retry on completion of this check? // should we schedule a retry on completion of this check?
bool scheduleRecheck = true; bool scheduleRecheck = true;
const string github_token = null; // TODO: populate. const string? github_token = null; // TODO: populate.
try try
{ {
@ -145,7 +143,7 @@ namespace osu.Desktop.Updater
private class UpdateCompleteNotification : ProgressCompletionNotification private class UpdateCompleteNotification : ProgressCompletionNotification
{ {
[Resolved] [Resolved]
private OsuGame game { get; set; } private OsuGame game { get; set; } = null!;
public UpdateCompleteNotification(SquirrelUpdateManager updateManager) public UpdateCompleteNotification(SquirrelUpdateManager updateManager)
{ {
@ -154,7 +152,7 @@ namespace osu.Desktop.Updater
Activated = () => Activated = () =>
{ {
updateManager.PrepareUpdateAsync() updateManager.PrepareUpdateAsync()
.ContinueWith(_ => updateManager.Schedule(() => game?.AttemptExit())); .ContinueWith(_ => updateManager.Schedule(() => game.AttemptExit()));
return true; return true;
}; };
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -14,12 +12,12 @@ namespace osu.Desktop.Windows
{ {
public class GameplayWinKeyBlocker : Component public class GameplayWinKeyBlocker : Component
{ {
private Bindable<bool> disableWinKey; private Bindable<bool> disableWinKey = null!;
private IBindable<bool> localUserPlaying; private IBindable<bool> localUserPlaying = null!;
private IBindable<bool> isActive; private IBindable<bool> isActive = null!;
[Resolved] [Resolved]
private GameHost host { get; set; } private GameHost host { get; set; } = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config) private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config)

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -21,7 +19,7 @@ namespace osu.Desktop.Windows
private const int wm_syskeyup = 261; private const int wm_syskeyup = 261;
//Resharper disable once NotAccessedField.Local //Resharper disable once NotAccessedField.Local
private static LowLevelKeyboardProcDelegate keyboardHookDelegate; // keeping a reference alive for the GC private static LowLevelKeyboardProcDelegate? keyboardHookDelegate; // keeping a reference alive for the GC
private static IntPtr keyHook; private static IntPtr keyHook;
[StructLayout(LayoutKind.Explicit)] [StructLayout(LayoutKind.Explicit)]

View File

@ -23,6 +23,7 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using System; using System;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Catch.Edit; using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Skinning.Legacy; using osu.Game.Rulesets.Catch.Skinning.Legacy;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -162,7 +163,7 @@ namespace osu.Game.Rulesets.Catch
}; };
} }
public override string GetDisplayNameForHitResult(HitResult result) public override LocalisableString GetDisplayNameForHitResult(HitResult result)
{ {
switch (result) switch (result)
{ {

View File

@ -1,12 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModEasy : ModEasyWithExtraLives public class CatchModEasy : ModEasyWithExtraLives
{ {
public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!"; public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
} }
} }

View File

@ -3,6 +3,7 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public override string Name => "Floating Fruits"; public override string Name => "Floating Fruits";
public override string Acronym => "FF"; public override string Acronym => "FF";
public override string Description => "The fruits are... floating?"; public override LocalisableString Description => "The fruits are... floating?";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override IconUsage? Icon => FontAwesome.Solid.Cloud; public override IconUsage? Icon => FontAwesome.Solid.Cloud;

View File

@ -3,6 +3,7 @@
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset<CatchHitObject> public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset<CatchHitObject>
{ {
public override string Description => @"Play with fading fruits."; public override LocalisableString Description => @"Play with fading fruits.";
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
private const double fade_out_offset_multiplier = 0.6; private const double fade_out_offset_multiplier = 0.6;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq; using System.Linq;
using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModMirror : ModMirror, IApplicableToBeatmap public class CatchModMirror : ModMirror, IApplicableToBeatmap
{ {
public override string Description => "Fruits are flipped horizontally."; public override LocalisableString Description => "Fruits are flipped horizontally.";
/// <remarks> /// <remarks>
/// <see cref="IApplicableToBeatmap"/> is used instead of <see cref="IApplicableToHitObject"/>, /// <see cref="IApplicableToBeatmap"/> is used instead of <see cref="IApplicableToHitObject"/>,

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModNoScope : ModNoScope, IUpdatableByPlayfield public class CatchModNoScope : ModNoScope, IUpdatableByPlayfield
{ {
public override string Description => "Where's the catcher?"; public override LocalisableString Description => "Where's the catcher?";
[SettingSource( [SettingSource(
"Hidden at combo", "Hidden at combo",

View File

@ -5,6 +5,7 @@ using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset<CatchHitObject>, IApplicableToPlayer public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset<CatchHitObject>, IApplicableToPlayer
{ {
public override string Description => @"Use the mouse to control the catcher."; public override LocalisableString Description => @"Use the mouse to control the catcher.";
private DrawableRuleset<CatchHitObject> drawableRuleset = null!; private DrawableRuleset<CatchHitObject> drawableRuleset = null!;

View File

@ -16,11 +16,11 @@ namespace osu.Game.Rulesets.Mania.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase(2.3449735700206298d, 242, "diffcalc-test")] [TestCase(2.3493769750220914d, 242, "diffcalc-test")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(2.7879104989252959d, 242, "diffcalc-test")] [TestCase(2.797245912537965d, 242, "diffcalc-test")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());

View File

@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public class TestSceneBarLine : ManiaSkinnableTestScene
{
[Test]
public void TestMinor()
{
AddStep("Create barlines", () => recreate());
}
private void recreate(Func<IEnumerable<BarLine>>? createBarLines = null)
{
var stageDefinitions = new List<StageDefinition>
{
new StageDefinition { Columns = 4 },
};
SetContents(_ => new ManiaPlayfield(stageDefinitions).With(s =>
{
if (createBarLines != null)
{
var barLines = createBarLines();
foreach (var b in barLines)
s.Add(b);
return;
}
for (int i = 0; i < 64; i++)
{
s.Add(new BarLine
{
StartTime = Time.Current + i * 500,
Major = i % 4 == 0,
});
}
}));
}
}
}

View File

@ -21,7 +21,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
protected override double SkillMultiplier => 1; protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 1; protected override double StrainDecayBase => 1;
private readonly double[] holdEndTimes; private readonly double[] startTimes;
private readonly double[] endTimes;
private readonly double[] individualStrains; private readonly double[] individualStrains;
private double individualStrain; private double individualStrain;
@ -30,7 +31,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
public Strain(Mod[] mods, int totalColumns) public Strain(Mod[] mods, int totalColumns)
: base(mods) : base(mods)
{ {
holdEndTimes = new double[totalColumns]; startTimes = new double[totalColumns];
endTimes = new double[totalColumns];
individualStrains = new double[totalColumns]; individualStrains = new double[totalColumns];
overallStrain = 1; overallStrain = 1;
} }
@ -38,32 +40,27 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current) protected override double StrainValueOf(DifficultyHitObject current)
{ {
var maniaCurrent = (ManiaDifficultyHitObject)current; var maniaCurrent = (ManiaDifficultyHitObject)current;
double startTime = maniaCurrent.StartTime;
double endTime = maniaCurrent.EndTime; double endTime = maniaCurrent.EndTime;
int column = maniaCurrent.BaseObject.Column; int column = maniaCurrent.BaseObject.Column;
double closestEndTime = Math.Abs(endTime - maniaCurrent.LastObject.StartTime); // Lowest value we can assume with the current information
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
bool isOverlapping = false; bool isOverlapping = false;
// Fill up the holdEndTimes array double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information
for (int i = 0; i < holdEndTimes.Length; ++i) 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
for (int i = 0; i < endTimes.Length; ++i)
{ {
// The current note is overlapped if a previous note or end is overlapping the current note body // The current note is overlapped if a previous note or end is overlapping the current note body
isOverlapping |= Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1); isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) && Precision.DefinitelyBigger(endTime, endTimes[i], 1);
// We give a slight bonus to everything if something is held meanwhile // We give a slight bonus to everything if something is held meanwhile
if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1)) if (Precision.DefinitelyBigger(endTimes[i], endTime, 1))
holdFactor = 1.25; holdFactor = 1.25;
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - holdEndTimes[i])); closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i]));
// Decay individual strains
individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
} }
holdEndTimes[column] = endTime;
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending. // The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away. // Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
// holdAddition // holdAddition
@ -77,12 +74,22 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
if (isOverlapping) if (isOverlapping)
holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime))); holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime)));
// Increase individual strain in own column // Decay and increase individualStrains in own column
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);
individualStrains[column] += 2.0 * holdFactor; individualStrains[column] += 2.0 * holdFactor;
individualStrain = individualStrains[column];
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor; // For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns
individualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(individualStrain, individualStrains[column]) : individualStrains[column];
// Decay and increase overallStrain
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base);
overallStrain += (1 + holdAddition) * holdFactor;
// Update startTimes and endTimes arrays
startTimes[column] = startTime;
endTimes[column] = endTime;
// By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
return individualStrain + overallStrain - CurrentStrain; return individualStrain + overallStrain - CurrentStrain;
} }

View File

@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
private void updateBeatmap() private void updateBeatmap()
{ {
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value; Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
Beatmap.SaveState();
} }
} }
} }

View File

@ -15,6 +15,7 @@ using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
@ -311,7 +312,7 @@ namespace osu.Game.Rulesets.Mania
return Array.Empty<KeyBinding>(); return Array.Empty<KeyBinding>();
} }
public override string GetVariantName(int variant) public override LocalisableString GetVariantName(int variant)
{ {
switch (getPlayfieldType(variant)) switch (getPlayfieldType(variant))
{ {
@ -356,7 +357,7 @@ namespace osu.Game.Rulesets.Mania
}; };
} }
public override string GetDisplayNameForHitResult(HitResult result) public override LocalisableString GetDisplayNameForHitResult(HitResult result)
{ {
switch (result) switch (result)
{ {

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
@ -16,9 +17,9 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Acronym => "CS"; public override string Acronym => "CS";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 0.9;
public override string Description => "No more tricky speed changes!"; public override LocalisableString Description => "No more tricky speed changes!";
public override IconUsage? Icon => FontAwesome.Solid.Equals; public override IconUsage? Icon => FontAwesome.Solid.Equals;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public override string Name => "Dual Stages"; public override string Name => "Dual Stages";
public override string Acronym => "DS"; public override string Acronym => "DS";
public override string Description => @"Double the stages, double the fun!"; public override LocalisableString Description => @"Double the stages, double the fun!";
public override ModType Type => ModType.Conversion; public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;

View File

@ -1,12 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModEasy : ModEasyWithExtraLives public class ManiaModEasy : ModEasyWithExtraLives
{ {
public override string Description => @"More forgiving HP drain, less accuracy required, and three lives!"; public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and three lives!";
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public override string Name => "Fade In"; public override string Name => "Fade In";
public override string Acronym => "FI"; public override string Acronym => "FI";
public override string Description => @"Keys appear out of nowhere!"; public override LocalisableString Description => @"Keys appear out of nowhere!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray();

View File

@ -3,13 +3,14 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModHidden : ManiaModPlayfieldCover public class ManiaModHidden : ManiaModPlayfieldCover
{ {
public override string Description => @"Keys fade out before you hit them!"; public override LocalisableString Description => @"Keys fade out before you hit them!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray();

View File

@ -8,6 +8,7 @@ using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override string Description => @"Replaces all hold notes with normal notes."; public override LocalisableString Description => @"Replaces all hold notes with normal notes.";
public override IconUsage? Icon => FontAwesome.Solid.DotCircle; public override IconUsage? Icon => FontAwesome.Solid.DotCircle;

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Acronym => "IN"; public override string Acronym => "IN";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override string Description => "Hold the keys. To the beat."; public override LocalisableString Description => "Hold the keys. To the beat.";
public override IconUsage? Icon => FontAwesome.Solid.YinYang; public override IconUsage? Icon => FontAwesome.Solid.YinYang;

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModKey1 : ManiaKeyMod public class ManiaModKey1 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 1; public override int KeyCount => 1;
public override string Name => "One Key"; public override string Name => "One Key";
public override string Acronym => "1K"; public override string Acronym => "1K";
public override string Description => @"Play with one key."; public override LocalisableString Description => @"Play with one key.";
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModKey10 : ManiaKeyMod public class ManiaModKey10 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 10; public override int KeyCount => 10;
public override string Name => "Ten Keys"; public override string Name => "Ten Keys";
public override string Acronym => "10K"; public override string Acronym => "10K";
public override string Description => @"Play with ten keys."; public override LocalisableString Description => @"Play with ten keys.";
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModKey2 : ManiaKeyMod public class ManiaModKey2 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 2; public override int KeyCount => 2;
public override string Name => "Two Keys"; public override string Name => "Two Keys";
public override string Acronym => "2K"; public override string Acronym => "2K";
public override string Description => @"Play with two keys."; public override LocalisableString Description => @"Play with two keys.";
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModKey3 : ManiaKeyMod public class ManiaModKey3 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 3; public override int KeyCount => 3;
public override string Name => "Three Keys"; public override string Name => "Three Keys";
public override string Acronym => "3K"; public override string Acronym => "3K";
public override string Description => @"Play with three keys."; public override LocalisableString Description => @"Play with three keys.";
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModKey4 : ManiaKeyMod public class ManiaModKey4 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 4; public override int KeyCount => 4;
public override string Name => "Four Keys"; public override string Name => "Four Keys";
public override string Acronym => "4K"; public override string Acronym => "4K";
public override string Description => @"Play with four keys."; public override LocalisableString Description => @"Play with four keys.";
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModKey5 : ManiaKeyMod public class ManiaModKey5 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 5; public override int KeyCount => 5;
public override string Name => "Five Keys"; public override string Name => "Five Keys";
public override string Acronym => "5K"; public override string Acronym => "5K";
public override string Description => @"Play with five keys."; public override LocalisableString Description => @"Play with five keys.";
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModKey6 : ManiaKeyMod public class ManiaModKey6 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 6; public override int KeyCount => 6;
public override string Name => "Six Keys"; public override string Name => "Six Keys";
public override string Acronym => "6K"; public override string Acronym => "6K";
public override string Description => @"Play with six keys."; public override LocalisableString Description => @"Play with six keys.";
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModKey7 : ManiaKeyMod public class ManiaModKey7 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 7; public override int KeyCount => 7;
public override string Name => "Seven Keys"; public override string Name => "Seven Keys";
public override string Acronym => "7K"; public override string Acronym => "7K";
public override string Description => @"Play with seven keys."; public override LocalisableString Description => @"Play with seven keys.";
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModKey8 : ManiaKeyMod public class ManiaModKey8 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 8; public override int KeyCount => 8;
public override string Name => "Eight Keys"; public override string Name => "Eight Keys";
public override string Acronym => "8K"; public override string Acronym => "8K";
public override string Description => @"Play with eight keys."; public override LocalisableString Description => @"Play with eight keys.";
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModKey9 : ManiaKeyMod public class ManiaModKey9 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 9; public override int KeyCount => 9;
public override string Name => "Nine Keys"; public override string Name => "Nine Keys";
public override string Acronym => "9K"; public override string Acronym => "9K";
public override string Description => @"Play with nine keys."; public override LocalisableString Description => @"Play with nine keys.";
} }
} }

View File

@ -5,6 +5,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using System.Linq; using System.Linq;
using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModMirror : ModMirror, IApplicableToBeatmap public class ManiaModMirror : ModMirror, IApplicableToBeatmap
{ {
public override string Description => "Notes are flipped horizontally."; public override LocalisableString Description => "Notes are flipped horizontally.";
public void ApplyToBeatmap(IBeatmap beatmap) public void ApplyToBeatmap(IBeatmap beatmap)
{ {

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModRandom : ModRandom, IApplicableToBeatmap public class ManiaModRandom : ModRandom, IApplicableToBeatmap
{ {
public override string Description => @"Shuffle around the keys!"; public override LocalisableString Description => @"Shuffle around the keys!";
public void ApplyToBeatmap(IBeatmap beatmap) public void ApplyToBeatmap(IBeatmap beatmap)
{ {

View File

@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
@ -16,21 +13,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary> /// </summary>
public class DrawableBarLine : DrawableManiaHitObject<BarLine> public class DrawableBarLine : DrawableManiaHitObject<BarLine>
{ {
/// <summary>
/// Height of major bar line triangles.
/// </summary>
private const float triangle_height = 12;
/// <summary>
/// Offset of the major bar line triangles from the sides of the bar line.
/// </summary>
private const float triangle_offset = 9;
public DrawableBarLine(BarLine barLine) public DrawableBarLine(BarLine barLine)
: base(barLine) : base(barLine)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = 2f; Height = barLine.Major ? 1.7f : 1.2f;
AddInternal(new Box AddInternal(new Box
{ {
@ -38,34 +25,33 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = new Color4(255, 204, 33, 255), Alpha = barLine.Major ? 0.5f : 0.2f
}); });
if (barLine.Major) if (barLine.Major)
{ {
AddInternal(new EquilateralTriangle Vector2 size = new Vector2(22, 6);
const float line_offset = 4;
AddInternal(new Circle
{ {
Name = "Left triangle", Name = "Left line",
Anchor = Anchor.BottomLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.TopCentre, Origin = Anchor.CentreRight,
Size = new Vector2(triangle_height),
X = -triangle_offset, Size = size,
Rotation = 90 X = -line_offset,
}); });
AddInternal(new EquilateralTriangle AddInternal(new Circle
{ {
Name = "Right triangle", Name = "Right line",
Anchor = Anchor.BottomRight, Anchor = Anchor.CentreRight,
Origin = Anchor.TopCentre, Origin = Anchor.CentreLeft,
Size = new Vector2(triangle_height), Size = size,
X = triangle_offset, X = line_offset,
Rotation = -90
}); });
} }
if (!barLine.Major)
Alpha = 0.2f;
} }
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()

View File

@ -9,7 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;

View File

@ -9,7 +9,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;

View File

@ -0,0 +1,213 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneObjectMerging : TestSceneOsuEditor
{
[Test]
public void TestSimpleMerge()
{
HitCircle? circle1 = null;
HitCircle? circle2 = null;
AddStep("select first two circles", () =>
{
circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle);
circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1);
EditorClock.Seek(circle1.StartTime);
EditorBeatmap.SelectedHitObjects.Add(circle1);
EditorBeatmap.SelectedHitObjects.Add(circle2);
});
mergeSelection();
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
(pos: circle1.Position, pathType: PathType.Linear),
(pos: circle2.Position, pathType: null)));
AddStep("undo", () => Editor.Undo());
AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && objectsRestored(circle1, circle2));
}
[Test]
public void TestMergeCircleSlider()
{
HitCircle? circle1 = null;
Slider? slider = null;
HitCircle? circle2 = null;
AddStep("select a circle, slider, circle", () =>
{
circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle);
slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > circle1.StartTime);
circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h.StartTime > slider.StartTime);
EditorClock.Seek(circle1.StartTime);
EditorBeatmap.SelectedHitObjects.Add(circle1);
EditorBeatmap.SelectedHitObjects.Add(slider);
EditorBeatmap.SelectedHitObjects.Add(circle2);
});
mergeSelection();
AddAssert("slider created", () =>
{
if (circle1 is null || circle2 is null || slider is null)
return false;
var controlPoints = slider.Path.ControlPoints;
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2];
args[0] = (circle1.Position, PathType.Linear);
for (int i = 0; i < controlPoints.Count; i++)
{
args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.Linear : controlPoints[i].Type);
}
args[^1] = (circle2.Position, null);
return sliderCreatedFor(args);
});
AddStep("undo", () => Editor.Undo());
AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && slider is not null && objectsRestored(circle1, slider, circle2));
}
[Test]
public void TestMergeSliderSlider()
{
Slider? slider1 = null;
SliderPath? slider1Path = null;
Slider? slider2 = null;
AddStep("select two sliders", () =>
{
slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value);
slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime);
EditorClock.Seek(slider1.StartTime);
EditorBeatmap.SelectedHitObjects.Add(slider1);
EditorBeatmap.SelectedHitObjects.Add(slider2);
});
mergeSelection();
AddAssert("slider created", () =>
{
if (slider1 is null || slider2 is null || slider1Path is null)
return false;
var controlPoints1 = slider1Path.ControlPoints;
var controlPoints2 = slider2.Path.ControlPoints;
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1];
for (int i = 0; i < controlPoints1.Count - 1; i++)
{
args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type);
}
for (int i = 0; i < controlPoints2.Count; i++)
{
args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type);
}
return sliderCreatedFor(args);
});
AddAssert("merged slider matches first slider", () =>
{
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
&& mergedSlider.Samples.SequenceEqual(slider1.Samples)
&& mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint);
});
AddAssert("slider end is at same completion for last slider", () =>
{
if (slider1Path is null || slider2 is null)
return false;
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance);
});
}
[Test]
public void TestNonMerge()
{
HitCircle? circle1 = null;
HitCircle? circle2 = null;
Spinner? spinner = null;
AddStep("select first two circles and spinner", () =>
{
circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle);
circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1);
spinner = (Spinner)EditorBeatmap.HitObjects.First(h => h is Spinner);
EditorClock.Seek(spinner.StartTime);
EditorBeatmap.SelectedHitObjects.Add(circle1);
EditorBeatmap.SelectedHitObjects.Add(circle2);
EditorBeatmap.SelectedHitObjects.Add(spinner);
});
mergeSelection();
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
(pos: circle1.Position, pathType: PathType.Linear),
(pos: circle2.Position, pathType: null)));
AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner));
}
private void mergeSelection()
{
AddStep("merge selection", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.M);
InputManager.ReleaseKey(Key.LShift);
InputManager.ReleaseKey(Key.LControl);
});
}
private bool sliderCreatedFor(params (Vector2 pos, PathType? pathType)[] expectedControlPoints)
{
if (EditorBeatmap.SelectedHitObjects.Count != 1)
return false;
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
int i = 0;
foreach ((Vector2 pos, PathType? pathType) in expectedControlPoints)
{
var controlPoint = mergedSlider.Path.ControlPoints[i++];
if (!Precision.AlmostEquals(controlPoint.Position + mergedSlider.Position, pos) || controlPoint.Type != pathType)
return false;
}
return true;
}
private bool objectsRestored(params HitObject[] objects)
{
foreach (var hitObject in objects)
{
if (EditorBeatmap.HitObjects.Contains(hitObject))
return false;
}
return true;
}
}
}

View File

@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -55,9 +56,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
ControlPoints = ControlPoints =
{ {
new PathControlPoint(Vector2.Zero), new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(OsuPlayfield.BASE_SIZE * 2 / 5), new PathControlPoint(new Vector2(136, 205)),
new PathControlPoint(OsuPlayfield.BASE_SIZE * 3 / 5) new PathControlPoint(new Vector2(-4, 226))
} }
} }
})); }));
@ -99,8 +100,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("move mouse to new point location", () => AddStep("move mouse to new point location", () =>
{ {
var firstPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]); var firstPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]);
var secondPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[1]); var pos = slider.Path.PositionAt(0.25d) + slider.Position;
InputManager.MoveMouseTo((firstPiece.ScreenSpaceDrawQuad.Centre + secondPiece.ScreenSpaceDrawQuad.Centre) / 2); InputManager.MoveMouseTo(firstPiece.Parent.ToScreenSpace(pos));
}); });
AddStep("move slider end", () => AddStep("move slider end", () =>
{ {
@ -175,6 +176,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertSliderSnapped(false); assertSliderSnapped(false);
} }
[Test]
public void TestRotatingSliderRetainsPerfectControlPointType()
{
OsuSelectionHandler selectionHandler;
AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("rotate 90 degrees ccw", () =>
{
selectionHandler = this.ChildrenOfType<OsuSelectionHandler>().Single();
selectionHandler.HandleRotation(-90);
});
AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
}
[Test] [Test]
public void TestFlippingSliderDoesNotSnap() public void TestFlippingSliderDoesNotSnap()
{ {
@ -200,6 +218,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertSliderSnapped(false); assertSliderSnapped(false);
} }
[Test]
public void TestFlippingSliderRetainsPerfectControlPointType()
{
OsuSelectionHandler selectionHandler;
AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("flip slider horizontally", () =>
{
selectionHandler = this.ChildrenOfType<OsuSelectionHandler>().Single();
selectionHandler.OnPressed(new KeyBindingPressEvent<GlobalAction>(InputManager.CurrentState, GlobalAction.EditorFlipVertically));
});
AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
}
[Test] [Test]
public void TestReversingSliderDoesNotSnap() public void TestReversingSliderDoesNotSnap()
{ {

View File

@ -6,7 +6,8 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Allocation;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Tests
[HeadlessTest] [HeadlessTest]
public class LegacyMainCirclePieceTest : OsuTestScene public class LegacyMainCirclePieceTest : OsuTestScene
{ {
[Resolved]
private IRenderer renderer { get; set; } = null!;
private static readonly object?[][] texture_priority_cases = private static readonly object?[][] texture_priority_cases =
{ {
// default priority lookup // default priority lookup
@ -76,7 +80,12 @@ namespace osu.Game.Rulesets.Osu.Tests
skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase(); skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase();
skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>())) skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>()))
.Returns((string componentName, WrapMode _, WrapMode _) => new Texture(1, 1) { AssetName = componentName }); .Returns((string componentName, WrapMode _, WrapMode _) =>
{
var tex = renderer.CreateTexture(1, 1);
tex.AssetName = componentName;
return tex;
});
Child = new DependencyProvidingContainer Child = new DependencyProvidingContainer
{ {
@ -84,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Child = piece = new TestLegacyMainCirclePiece(priorityLookup), Child = piece = new TestLegacyMainCirclePiece(priorityLookup),
}; };
var sprites = this.ChildrenOfType<Sprite>().Where(s => s.Texture.AssetName != null).DistinctBy(s => s.Texture.AssetName).ToArray(); var sprites = this.ChildrenOfType<Sprite>().Where(s => !string.IsNullOrEmpty(s.Texture.AssetName)).DistinctBy(s => s.Texture.AssetName).ToArray();
Debug.Assert(sprites.Length <= 2); Debug.Assert(sprites.Length <= 2);
}); });

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
Mod = mod, Mod = mod,
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 && PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 &&
Precision.AlmostEquals(Player.GameplayClockContainer.GameplayClock.Rate, mod.SpeedChange.Value) Precision.AlmostEquals(Player.GameplayClockContainer.Rate, mod.SpeedChange.Value)
}); });
} }
} }

View File

@ -17,18 +17,18 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.6369583000323935d, 206, "diffcalc-test")] [TestCase(6.7115569159190587d, 206, "diffcalc-test")]
[TestCase(1.4476531024675374d, 45, "zero-length-sliders")] [TestCase(1.4391311903612753d, 45, "zero-length-sliders")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.8816128335486386d, 206, "diffcalc-test")] [TestCase(8.9757300665532966d, 206, "diffcalc-test")]
[TestCase(1.7540389962596916d, 45, "zero-length-sliders")] [TestCase(1.7437232654020756d, 45, "zero-length-sliders")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.6369583000323935d, 239, "diffcalc-test")] [TestCase(6.7115569159190587d, 239, "diffcalc-test")]
[TestCase(1.4476531024675374d, 54, "zero-length-sliders")] [TestCase(1.4391311903612753d, 54, "zero-length-sliders")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());

View File

@ -11,7 +11,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing.Input; using osu.Framework.Testing.Input;
using osu.Game.Audio; using osu.Game.Audio;
@ -25,6 +25,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public class TestSceneCursorTrail : OsuTestScene public class TestSceneCursorTrail : OsuTestScene
{ {
[Resolved]
private IRenderer renderer { get; set; }
[Test] [Test]
public void TestSmoothCursorTrail() public void TestSmoothCursorTrail()
{ {
@ -44,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
createTest(() => createTest(() =>
{ {
var skinContainer = new LegacySkinContainer(false); var skinContainer = new LegacySkinContainer(renderer, false);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer); var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail; skinContainer.Child = legacyCursorTrail;
@ -58,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
createTest(() => createTest(() =>
{ {
var skinContainer = new LegacySkinContainer(true); var skinContainer = new LegacySkinContainer(renderer, true);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer); var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail; skinContainer.Child = legacyCursorTrail;
@ -82,10 +85,12 @@ namespace osu.Game.Rulesets.Osu.Tests
[Cached(typeof(ISkinSource))] [Cached(typeof(ISkinSource))]
private class LegacySkinContainer : Container, ISkinSource private class LegacySkinContainer : Container, ISkinSource
{ {
private readonly IRenderer renderer;
private readonly bool disjoint; private readonly bool disjoint;
public LegacySkinContainer(bool disjoint) public LegacySkinContainer(IRenderer renderer, bool disjoint)
{ {
this.renderer = renderer;
this.disjoint = disjoint; this.disjoint = disjoint;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -98,14 +103,14 @@ namespace osu.Game.Rulesets.Osu.Tests
switch (componentName) switch (componentName)
{ {
case "cursortrail": case "cursortrail":
var tex = new Texture(Texture.WhitePixel.TextureGL); var tex = new Texture(renderer.WhitePixel);
if (disjoint) if (disjoint)
tex.ScaleAdjust = 1 / 25f; tex.ScaleAdjust = 1 / 25f;
return tex; return tex;
case "cursormiddle": case "cursormiddle":
return disjoint ? null : Texture.WhitePixel; return disjoint ? null : renderer.WhitePixel;
} }
return null; return null;

View File

@ -10,7 +10,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input; using osu.Framework.Input;

View File

@ -12,7 +12,6 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Timing; using osu.Framework.Timing;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -12,7 +10,6 @@ using osu.Framework.Audio;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -36,16 +33,16 @@ namespace osu.Game.Rulesets.Osu.Tests
private const double spinner_duration = 6000; private const double spinner_duration = 6000;
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } private AudioManager audioManager { get; set; } = null!;
protected override bool Autoplay => true; protected override bool Autoplay => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer(); protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
private DrawableSpinner drawableSpinner; private DrawableSpinner drawableSpinner = null!;
private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single(); private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single();
[SetUpSteps] [SetUpSteps]
@ -67,12 +64,12 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
}); });
AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100)); AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100));
AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100)); AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.Not.EqualTo(0).Within(100));
addSeekStep(0); addSeekStep(0);
AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance)); AddAssert("is disc rotation almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(0).Within(trackerRotationTolerance));
AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100)); AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(0).Within(100));
} }
[Test] [Test]
@ -100,20 +97,20 @@ namespace osu.Game.Rulesets.Osu.Tests
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in. // we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
// due to the exponential damping applied we're allowing a larger margin of error of about 10% // due to the exponential damping applied we're allowing a larger margin of error of about 10%
// (5% relative to the final rotation value, but we're half-way through the spin). // (5% relative to the final rotation value, but we're half-way through the spin).
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation / 2, trackerRotationTolerance)); () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance));
AddAssert("symbol rotation rewound", AddAssert("symbol rotation rewound",
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance)); () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation / 2).Within(spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation rewound", AddAssert("is cumulative rotation rewound",
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100)); () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100));
addSeekStep(spinner_start_time + 5000); addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same", AddAssert("is disc rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance)); () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance));
AddAssert("is symbol rotation almost same", AddAssert("is symbol rotation almost same",
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance)); () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation).Within(spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation almost same", AddAssert("is cumulative rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation, 100)); () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100));
} }
[Test] [Test]
@ -177,10 +174,10 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value); AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value);
addSeekStep(2000); addSeekStep(2000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0)); AddAssert("spm still valid", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(estimatedSpm).Within(1.0));
addSeekStep(1000); addSeekStep(1000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0)); AddAssert("spm still valid", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(estimatedSpm).Within(1.0));
} }
[TestCase(0.5)] [TestCase(0.5)]
@ -202,14 +199,14 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate); AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate);
addSeekStep(1000); addSeekStep(1000);
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05)); AddAssert("progress almost same", () => drawableSpinner.Progress, () => Is.EqualTo(expectedProgress).Within(0.05));
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0)); AddAssert("spm almost same", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(expectedSpm).Within(2.0));
} }
private void addSeekStep(double time) private void addSeekStep(double time)
{ {
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time)); AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(100));
} }
private void transformReplay(Func<Replay, Replay> replayTransformation) => AddStep("set replay", () => private void transformReplay(Func<Replay, Replay> replayTransformation) => AddStep("set replay", () =>

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.18.1" /> <PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
public static class AimEvaluator public static class AimEvaluator
{ {
private const double wide_angle_multiplier = 1.5; private const double wide_angle_multiplier = 1.5;
private const double acute_angle_multiplier = 2.0; private const double acute_angle_multiplier = 1.95;
private const double slider_multiplier = 1.5; private const double slider_multiplier = 1.35;
private const double velocity_change_multiplier = 0.75; private const double velocity_change_multiplier = 0.75;
/// <summary> /// <summary>
@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2); velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
} }
if (osuLastObj.TravelTime != 0) if (osuLastObj.BaseObject is Slider)
{ {
// Reward sliders based on velocity. // Reward sliders based on velocity.
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime; sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;

View File

@ -15,11 +15,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
private const double max_opacity_bonus = 0.4; private const double max_opacity_bonus = 0.4;
private const double hidden_bonus = 0.2; private const double hidden_bonus = 0.2;
private const double min_velocity = 0.5;
private const double slider_multiplier = 1.3;
/// <summary> /// <summary>
/// Evaluates the difficulty of memorising and hitting an object, based on: /// Evaluates the difficulty of memorising and hitting an object, based on:
/// <list type="bullet"> /// <list type="bullet">
/// <item><description>distance between the previous and current object,</description></item> /// <item><description>distance between a number of previous objects and the current object,</description></item>
/// <item><description>the visual opacity of the current object,</description></item> /// <item><description>the visual opacity of the current object,</description></item>
/// <item><description>length and speed of the current object (for sliders),</description></item>
/// <item><description>and whether the hidden mod is enabled.</description></item> /// <item><description>and whether the hidden mod is enabled.</description></item>
/// </list> /// </list>
/// </summary> /// </summary>
@ -73,6 +77,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (hidden) if (hidden)
result *= 1.0 + hidden_bonus; result *= 1.0 + hidden_bonus;
double sliderBonus = 0.0;
if (osuCurrent.BaseObject is Slider osuSlider)
{
// Invert the scaling factor to determine the true travel distance independent of circle size.
double pixelTravelDistance = osuSlider.LazyTravelDistance / scalingFactor;
// Reward sliders based on velocity.
sliderBonus = Math.Pow(Math.Max(0.0, pixelTravelDistance / osuCurrent.TravelTime - min_velocity), 0.5);
// Longer sliders require more memorisation.
sliderBonus *= pixelTravelDistance;
// Nerf sliders with repeats, as less memorisation is required.
if (osuSlider.RepeatCount > 0)
sliderBonus /= (osuSlider.RepeatCount + 1);
}
result += sliderBonus * slider_multiplier;
return result; return result;
} }
} }

View File

@ -46,7 +46,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
if (mods.Any(h => h is OsuModRelax)) if (mods.Any(h => h is OsuModRelax))
{
aimRating *= 0.9;
speedRating = 0.0; speedRating = 0.0;
flashlightRating *= 0.7;
}
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000; double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000; double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
@ -62,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1 Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
); );
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; double starRating = basePerformance > 0.00001 ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate; double drainRate = beatmap.Difficulty.DrainRate;

View File

@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
public class OsuPerformanceCalculator : PerformanceCalculator public class OsuPerformanceCalculator : PerformanceCalculator
{ {
public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
private double accuracy; private double accuracy;
private int scoreMaxCombo; private int scoreMaxCombo;
private int countGreat; private int countGreat;
@ -41,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
effectiveMissCount = calculateEffectiveMissCount(osuAttributes); effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. double multiplier = PERFORMANCE_BASE_MULTIPLIER;
if (score.Mods.Any(m => m is OsuModNoFail)) if (score.Mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount); multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
@ -51,10 +53,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(h => h is OsuModRelax)) if (score.Mods.Any(h => h is OsuModRelax))
{ {
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. // https://www.desmos.com/calculator/bc9eybdthb
effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits); // we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
// this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0);
double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0);
multiplier *= 0.6; // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
} }
double aimValue = computeAimValue(score, osuAttributes); double aimValue = computeAimValue(score, osuAttributes);
@ -103,7 +109,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (attributes.ApproachRate > 10.33) if (attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
else if (attributes.ApproachRate < 8.0) else if (attributes.ApproachRate < 8.0)
approachRateFactor = 0.1 * (8.0 - attributes.ApproachRate); approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate);
if (score.Mods.Any(h => h is OsuModRelax))
approachRateFactor = 0.0;
aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
@ -134,6 +143,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes) private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{ {
if (score.Mods.Any(h => h is OsuModRelax))
return 0.0;
double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
@ -174,7 +186,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2); speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping. // Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
return speedValue; return speedValue;
} }
@ -258,14 +270,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
} }
// Clamp miss count since it's derived from combo and can be higher than total hits and that breaks some calculations // Clamp miss count to maximum amount of possible breaks
comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits); comboBasedMissCount = Math.Min(comboBasedMissCount, countOk + countMeh + countMiss);
return Math.Max(countMiss, comboBasedMissCount); return Math.Max(countMiss, comboBasedMissCount);
} }
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
} }
} }

View File

@ -15,10 +15,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{ {
public class OsuDifficultyHitObject : DifficultyHitObject public class OsuDifficultyHitObject : DifficultyHitObject
{ {
private const int normalised_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. /// <summary>
/// A distance by which all distances should be scaled in order to assume a uniform circle size.
/// </summary>
public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
private const int min_delta_time = 25; private const int min_delta_time = 25;
private const float maximum_slider_radius = normalised_radius * 2.4f; private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
private const float assumed_slider_radius = normalised_radius * 1.8f; private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject; protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
@ -64,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
public double TravelDistance { get; private set; } public double TravelDistance { get; private set; }
/// <summary> /// <summary>
/// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for a non-zero distance. /// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for <see cref="Slider"/> objects.
/// </summary> /// </summary>
public double TravelTime { get; private set; } public double TravelTime { get; private set; }
@ -123,7 +127,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (BaseObject is Slider currentSlider) if (BaseObject is Slider currentSlider)
{ {
computeSliderCursorPosition(currentSlider); computeSliderCursorPosition(currentSlider);
TravelDistance = currentSlider.LazyTravelDistance; // Bonus for repeat sliders until a better per nested object strain system can be achieved.
TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time); TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time);
} }
@ -132,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
return; return;
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = normalised_radius / (float)BaseObject.Radius; float scalingFactor = NORMALISED_RADIUS / (float)BaseObject.Radius;
if (BaseObject.Radius < 30) if (BaseObject.Radius < 30)
{ {
@ -206,7 +211,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived. slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
var currCursorPosition = slider.StackedPosition; var currCursorPosition = slider.StackedPosition;
double scalingFactor = normalised_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used. double scalingFactor = NORMALISED_RADIUS / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
for (int i = 1; i < slider.NestedHitObjects.Count; i++) for (int i = 1; i < slider.NestedHitObjects.Count; i++)
{ {
@ -234,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
else if (currMovementObj is SliderRepeat) else if (currMovementObj is SliderRepeat)
{ {
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders. // For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
requiredMovement = normalised_radius; requiredMovement = NORMALISED_RADIUS;
} }
if (currMovementLength > requiredMovement) if (currMovementLength > requiredMovement)
@ -248,8 +253,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (i == slider.NestedHitObjects.Count - 1) if (i == slider.NestedHitObjects.Count - 1)
slider.LazyEndPosition = currCursorPosition; slider.LazyEndPosition = currCursorPosition;
} }
slider.LazyTravelDistance *= (float)Math.Pow(1 + slider.RepeatCount / 2.5, 1.0 / 2.5); // Bonus for repeat sliders until a better per nested object strain system can be achieved.
} }
private Vector2 getEndCursorPosition(OsuHitObject hitObject) private Vector2 getEndCursorPosition(OsuHitObject hitObject)

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double currentStrain; private double currentStrain;
private double skillMultiplier => 23.25; private double skillMultiplier => 23.55;
private double strainDecayBase => 0.15; private double strainDecayBase => 0.15;
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);

View File

@ -142,8 +142,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
foreach (var point in e.OldItems.Cast<PathControlPoint>()) foreach (var point in e.OldItems.Cast<PathControlPoint>())
{ {
Pieces.RemoveAll(p => p.ControlPoint == point); foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
Connections.RemoveAll(c => c.ControlPoint == point); piece.RemoveAndDisposeImmediately();
foreach (var connection in Connections.Where(c => c.ControlPoint == point).ToArray())
connection.RemoveAndDisposeImmediately();
} }
// If removing before the end of the path, // If removing before the end of the path,

View File

@ -251,13 +251,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void convertToStream() private void convertToStream()
{ {
if (editorBeatmap == null || changeHandler == null || beatDivisor == null) if (editorBeatmap == null || beatDivisor == null)
return; return;
var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime); var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime);
double streamSpacing = timingPoint.BeatLength / beatDivisor.Value; double streamSpacing = timingPoint.BeatLength / beatDivisor.Value;
changeHandler.BeginChange(); changeHandler?.BeginChange();
int i = 0; int i = 0;
double time = HitObject.StartTime; double time = HitObject.StartTime;
@ -292,7 +292,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
editorBeatmap.Remove(HitObject); editorBeatmap.Remove(HitObject);
changeHandler.EndChange(); changeHandler?.EndChange();
} }
public override MenuItem[] ContextMenuItems => new MenuItem[] public override MenuItem[] ContextMenuItems => new MenuItem[]

View File

@ -6,8 +6,11 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -15,6 +18,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit namespace osu.Game.Rulesets.Osu.Edit
{ {
@ -53,6 +57,17 @@ namespace osu.Game.Rulesets.Osu.Edit
referencePathTypes = null; referencePathTypes = null;
} }
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
{
mergeSelection();
return true;
}
return false;
}
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent) public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
{ {
var hitObjects = selectedMovableObjects; var hitObjects = selectedMovableObjects;
@ -112,13 +127,16 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
didFlip = true; didFlip = true;
foreach (var point in slider.Path.ControlPoints) var controlPoints = slider.Path.ControlPoints.Select(p =>
{ new PathControlPoint(new Vector2(
point.Position = new Vector2( (direction == Direction.Horizontal ? -1 : 1) * p.Position.X,
(direction == Direction.Horizontal ? -1 : 1) * point.Position.X, (direction == Direction.Vertical ? -1 : 1) * p.Position.Y
(direction == Direction.Vertical ? -1 : 1) * point.Position.Y ), p.Type)).ToArray();
);
} // Importantly, update as a single operation so automatic adjustment of control points to different
// curve types does not unexpectedly trigger and change the slider's shape.
slider.Path.ControlPoints.Clear();
slider.Path.ControlPoints.AddRange(controlPoints);
} }
} }
@ -168,8 +186,13 @@ namespace osu.Game.Rulesets.Osu.Edit
if (h is IHasPath path) if (h is IHasPath path)
{ {
foreach (var point in path.Path.ControlPoints) var controlPoints = path.Path.ControlPoints.Select(p =>
point.Position = RotatePointAroundOrigin(point.Position, Vector2.Zero, delta); new PathControlPoint(RotatePointAroundOrigin(p.Position, Vector2.Zero, delta), p.Type)).ToArray();
// Importantly, update as a single operation so automatic adjustment of control points to different
// curve types does not unexpectedly trigger and change the slider's shape.
path.Path.ControlPoints.Clear();
path.Path.ControlPoints.AddRange(controlPoints);
} }
} }
@ -320,7 +343,109 @@ namespace osu.Game.Rulesets.Osu.Edit
/// All osu! hitobjects which can be moved/rotated/scaled. /// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary> /// </summary>
private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType<OsuHitObject>() private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType<OsuHitObject>()
.Where(h => !(h is Spinner)) .Where(h => h is not Spinner)
.ToArray(); .ToArray();
/// <summary>
/// All osu! hitobjects which can be merged.
/// </summary>
private OsuHitObject[] selectedMergeableObjects => SelectedItems.OfType<OsuHitObject>()
.Where(h => h is HitCircle or Slider)
.OrderBy(h => h.StartTime)
.ToArray();
private void mergeSelection()
{
var mergeableObjects = selectedMergeableObjects;
if (mergeableObjects.Length < 2)
return;
ChangeHandler?.BeginChange();
// Have an initial slider object.
var firstHitObject = mergeableObjects[0];
var mergedHitObject = firstHitObject as Slider ?? new Slider
{
StartTime = firstHitObject.StartTime,
Position = firstHitObject.Position,
NewCombo = firstHitObject.NewCombo,
SampleControlPoint = firstHitObject.SampleControlPoint,
};
if (mergedHitObject.Path.ControlPoints.Count == 0)
{
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.Linear));
}
// Merge all the selected hit objects into one slider path.
bool lastCircle = firstHitObject is HitCircle;
foreach (var selectedMergeableObject in mergeableObjects.Skip(1))
{
if (selectedMergeableObject is IHasPath hasPath)
{
var offset = lastCircle ? selectedMergeableObject.Position - mergedHitObject.Position : mergedHitObject.Path.ControlPoints[^1].Position;
float distanceToLastControlPoint = Vector2.Distance(mergedHitObject.Path.ControlPoints[^1].Position, offset);
// Calculate the distance required to travel to the expected distance of the merging slider.
mergedHitObject.Path.ExpectedDistance.Value = mergedHitObject.Path.CalculatedDistance + distanceToLastControlPoint + hasPath.Path.Distance;
// Remove the last control point if it sits exactly on the start of the next control point.
if (Precision.AlmostEquals(distanceToLastControlPoint, 0))
{
mergedHitObject.Path.ControlPoints.RemoveAt(mergedHitObject.Path.ControlPoints.Count - 1);
}
mergedHitObject.Path.ControlPoints.AddRange(hasPath.Path.ControlPoints.Select(o => new PathControlPoint(o.Position + offset, o.Type)));
lastCircle = false;
}
else
{
// Turn the last control point into a linear type if this is the first merging circle in a sequence, so the subsequent control points can be inherited path type.
if (!lastCircle)
{
mergedHitObject.Path.ControlPoints.Last().Type = PathType.Linear;
}
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position));
mergedHitObject.Path.ExpectedDistance.Value = null;
lastCircle = true;
}
}
// Make sure only the merged hit object is in the beatmap.
if (firstHitObject is Slider)
{
foreach (var selectedMergeableObject in mergeableObjects.Skip(1))
{
EditorBeatmap.Remove(selectedMergeableObject);
}
}
else
{
foreach (var selectedMergeableObject in mergeableObjects)
{
EditorBeatmap.Remove(selectedMergeableObject);
}
EditorBeatmap.Add(mergedHitObject);
}
// Make sure the merged hitobject is selected.
SelectedItems.Clear();
SelectedItems.Add(mergedHitObject);
ChangeHandler?.EndChange();
}
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
{
foreach (var item in base.GetContextMenuItemsForSelection(selection))
yield return item;
if (selectedMergeableObjects.Length > 1)
yield return new OsuMenuItem("Merge selection", MenuItemType.Destructive, mergeSelection);
}
} }
} }

View File

@ -49,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
private void updateBeatmap() private void updateBeatmap()
{ {
Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value; Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value;
Beatmap.SaveState();
} }
} }
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public override string Name => @"Alternate"; public override string Name => @"Alternate";
public override string Acronym => @"AL"; public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!"; public override LocalisableString Description => @"Don't use the same key twice in a row!";
public override IconUsage? Icon => FontAwesome.Solid.Keyboard; public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public override string Name => "Approach Different"; public override string Name => "Approach Different";
public override string Acronym => "AD"; public override string Acronym => "AD";
public override string Description => "Never trust the approach circles..."; public override LocalisableString Description => "Never trust the approach circles...";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.StateChanges; using osu.Framework.Input.StateChanges;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -20,8 +21,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "AP"; public override string Acronym => "AP";
public override IconUsage? Icon => OsuIcon.ModAutopilot; public override IconUsage? Icon => OsuIcon.ModAutopilot;
public override ModType Type => ModType.Automation; public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm."; public override LocalisableString Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 0.1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) };
public bool PerformFail() => false; public bool PerformFail() => false;
@ -30,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Mods
private OsuInputManager inputManager = null!; private OsuInputManager inputManager = null!;
private IFrameStableClock gameplayClock = null!;
private List<OsuReplayFrame> replayFrames = null!; private List<OsuReplayFrame> replayFrames = null!;
private int currentFrame; private int currentFrame;
@ -40,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
if (currentFrame == replayFrames.Count - 1) return; if (currentFrame == replayFrames.Count - 1) return;
double time = gameplayClock.CurrentTime; double time = playfield.Clock.CurrentTime;
// Very naive implementation of autopilot based on proximity to replay frames. // Very naive implementation of autopilot based on proximity to replay frames.
// TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered). // TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered).
@ -55,8 +54,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{ {
gameplayClock = drawableRuleset.FrameStableClock;
// Grab the input manager to disable the user's cursor, and for future use // Grab the input manager to disable the user's cursor, and for future use
inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
inputManager.AllowUserCursorMovement = false; inputManager.AllowUserCursorMovement = false;

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModBlinds : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToHealthProcessor public class OsuModBlinds : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToHealthProcessor
{ {
public override string Name => "Blinds"; public override string Name => "Blinds";
public override string Description => "Play with blinds on your screen."; public override LocalisableString Description => "Play with blinds on your screen.";
public override string Acronym => "BL"; public override string Acronym => "BL";
public override IconUsage? Icon => FontAwesome.Solid.Adjust; public override IconUsage? Icon => FontAwesome.Solid.Adjust;

View File

@ -3,6 +3,7 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => FontAwesome.Solid.CompressArrowsAlt; public override IconUsage? Icon => FontAwesome.Solid.CompressArrowsAlt;
public override string Description => "Hit them at the right size!"; public override LocalisableString Description => "Hit them at the right size!";
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")] [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
public override BindableNumber<float> StartScale { get; } = new BindableFloat public override BindableNumber<float> StartScale { get; } = new BindableFloat

View File

@ -1,12 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModEasy : ModEasyWithExtraLives public class OsuModEasy : ModEasyWithExtraLives
{ {
public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!"; public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
} }
} }

View File

@ -3,6 +3,7 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => FontAwesome.Solid.ArrowsAltV; public override IconUsage? Icon => FontAwesome.Solid.ArrowsAltV;
public override string Description => "Hit them at the right size!"; public override LocalisableString Description => "Hit them at the right size!";
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")] [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
public override BindableNumber<float> StartScale { get; } = new BindableFloat public override BindableNumber<float> StartScale { get; } = new BindableFloat

View File

@ -6,6 +6,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
[SettingSource("Only fade approach circles", "The main object body will not fade when enabled.")] [SettingSource("Only fade approach circles", "The main object body will not fade when enabled.")]
public Bindable<bool> OnlyFadeApproachCircles { get; } = new BindableBool(); public Bindable<bool> OnlyFadeApproachCircles { get; } = new BindableBool();
public override string Description => @"Play with no approach circles and fading circles/sliders."; public override LocalisableString Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) }; public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };

View File

@ -4,6 +4,8 @@
using System; using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Timing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -22,12 +24,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "MG"; public override string Acronym => "MG";
public override IconUsage? Icon => FontAwesome.Solid.Magnet; public override IconUsage? Icon => FontAwesome.Solid.Magnet;
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circles your cursor is a magnet!"; public override LocalisableString Description => "No need to chase the circles your cursor is a magnet!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
private IFrameStableClock gameplayClock = null!;
[SettingSource("Attraction strength", "How strong the pull is.", 0)] [SettingSource("Attraction strength", "How strong the pull is.", 0)]
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f) public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
{ {
@ -38,8 +38,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{ {
gameplayClock = drawableRuleset.FrameStableClock;
// Hide judgment displays and follow points as they won't make any sense. // Hide judgment displays and follow points as they won't make any sense.
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
drawableRuleset.Playfield.DisplayJudgements.Value = false; drawableRuleset.Playfield.DisplayJudgements.Value = false;
@ -55,27 +53,27 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable) switch (drawable)
{ {
case DrawableHitCircle circle: case DrawableHitCircle circle:
easeTo(circle, cursorPos); easeTo(playfield.Clock, circle, cursorPos);
break; break;
case DrawableSlider slider: case DrawableSlider slider:
if (!slider.HeadCircle.Result.HasResult) if (!slider.HeadCircle.Result.HasResult)
easeTo(slider, cursorPos); easeTo(playfield.Clock, slider, cursorPos);
else else
easeTo(slider, cursorPos - slider.Ball.DrawPosition); easeTo(playfield.Clock, slider, cursorPos - slider.Ball.DrawPosition);
break; break;
} }
} }
} }
private void easeTo(DrawableHitObject hitObject, Vector2 destination) private void easeTo(IFrameBasedClock clock, DrawableHitObject hitObject, Vector2 destination)
{ {
double dampLength = Interpolation.Lerp(3000, 40, AttractionStrength.Value); double dampLength = Interpolation.Lerp(3000, 40, AttractionStrength.Value);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime); float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, clock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime); float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime);
hitObject.Position = new Vector2(x, y); hitObject.Position = new Vector2(x, y);
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModMirror : ModMirror, IApplicableToHitObject public class OsuModMirror : ModMirror, IApplicableToHitObject
{ {
public override string Description => "Flip objects on the chosen axes."; public override LocalisableString Description => "Flip objects on the chosen axes.";
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) }; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) };
[SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")] [SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")]

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModNoScope : ModNoScope, IUpdatableByPlayfield, IApplicableToBeatmap public class OsuModNoScope : ModNoScope, IUpdatableByPlayfield, IApplicableToBeatmap
{ {
public override string Description => "Where's the cursor?"; public override LocalisableString Description => "Where's the cursor?";
private PeriodTracker spinnerPeriods = null!; private PeriodTracker spinnerPeriods = null!;

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
/// </summary> /// </summary>
public class OsuModRandom : ModRandom, IApplicableToBeatmap public class OsuModRandom : ModRandom, IApplicableToBeatmap
{ {
public override string Description => "It never gets boring!"; public override LocalisableString Description => "It never gets boring!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray();

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{ {
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
/// <summary> /// <summary>

View File

@ -2,8 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Timing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -22,12 +23,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Name => "Repel"; public override string Name => "Repel";
public override string Acronym => "RP"; public override string Acronym => "RP";
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "Hit objects run away!"; public override LocalisableString Description => "Hit objects run away!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) };
private IFrameStableClock? gameplayClock;
[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)] [SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f) public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)
{ {
@ -38,8 +37,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{ {
gameplayClock = drawableRuleset.FrameStableClock;
// Hide judgment displays and follow points as they won't make any sense. // Hide judgment displays and follow points as they won't make any sense.
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
drawableRuleset.Playfield.DisplayJudgements.Value = false; drawableRuleset.Playfield.DisplayJudgements.Value = false;
@ -68,29 +65,27 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable) switch (drawable)
{ {
case DrawableHitCircle circle: case DrawableHitCircle circle:
easeTo(circle, destination, cursorPos); easeTo(playfield.Clock, circle, destination, cursorPos);
break; break;
case DrawableSlider slider: case DrawableSlider slider:
if (!slider.HeadCircle.Result.HasResult) if (!slider.HeadCircle.Result.HasResult)
easeTo(slider, destination, cursorPos); easeTo(playfield.Clock, slider, destination, cursorPos);
else else
easeTo(slider, destination - slider.Ball.DrawPosition, cursorPos); easeTo(playfield.Clock, slider, destination - slider.Ball.DrawPosition, cursorPos);
break; break;
} }
} }
} }
private void easeTo(DrawableHitObject hitObject, Vector2 destination, Vector2 cursorPos) private void easeTo(IFrameBasedClock clock, DrawableHitObject hitObject, Vector2 destination, Vector2 cursorPos)
{ {
Debug.Assert(gameplayClock != null);
double dampLength = Vector2.Distance(hitObject.Position, cursorPos) / (0.04 * RepulsionStrength.Value + 0.04); double dampLength = Vector2.Distance(hitObject.Position, cursorPos) / (0.04 * RepulsionStrength.Value + 0.04);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime); float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, clock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime); float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime);
hitObject.Position = new Vector2(x, y); hitObject.Position = new Vector2(x, y);
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
@ -10,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public override string Name => @"Single Tap"; public override string Name => @"Single Tap";
public override string Acronym => @"SG"; public override string Acronym => @"SG";
public override string Description => @"You must only use one key!"; public override LocalisableString Description => @"You must only use one key!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray();
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action; protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action;

View File

@ -4,6 +4,7 @@
using System; using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "SI"; public override string Acronym => "SI";
public override IconUsage? Icon => FontAwesome.Solid.Undo; public override IconUsage? Icon => FontAwesome.Solid.Undo;
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "Circles spin in. No approach circles."; public override LocalisableString Description => "Circles spin in. No approach circles.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
// todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque, // todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque,

View File

@ -4,6 +4,7 @@
using System; using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "SO"; public override string Acronym => "SO";
public override IconUsage? Icon => OsuIcon.ModSpunOut; public override IconUsage? Icon => OsuIcon.ModSpunOut;
public override ModType Type => ModType.Automation; public override ModType Type => ModType.Automation;
public override string Description => @"Spinners will be automatically completed."; public override LocalisableString Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9; public override double ScoreMultiplier => 0.9;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTarget) }; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTarget) };

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Name => @"Strict Tracking"; public override string Name => @"Strict Tracking";
public override string Acronym => @"ST"; public override string Acronym => @"ST";
public override ModType Type => ModType.DifficultyIncrease; public override ModType Type => ModType.DifficultyIncrease;
public override string Description => @"Once you start a slider, follow precisely or get a miss."; public override LocalisableString Description => @"Once you start a slider, follow precisely or get a miss.";
public override double ScoreMultiplier => 1.0; public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) }; public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) };

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "TP"; public override string Acronym => "TP";
public override ModType Type => ModType.Conversion; public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => OsuIcon.ModTarget; public override IconUsage? Icon => OsuIcon.ModTarget;
public override string Description => @"Practice keeping up with the beat of the song."; public override LocalisableString Description => @"Practice keeping up with the beat of the song.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
@ -9,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public override string Name => "Touch Device"; public override string Name => "Touch Device";
public override string Acronym => "TD"; public override string Acronym => "TD";
public override string Description => "Automatically applied to plays on devices with a touchscreen."; public override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override ModType Type => ModType.System; public override ModType Type => ModType.System;

View File

@ -4,6 +4,7 @@
using System; using System;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Name => "Traceable"; public override string Name => "Traceable";
public override string Acronym => "TC"; public override string Acronym => "TC";
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "Put your faith in the approach circles..."; public override LocalisableString Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) }; public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };

View File

@ -4,6 +4,7 @@
using System; using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "TR"; public override string Acronym => "TR";
public override IconUsage? Icon => FontAwesome.Solid.ArrowsAlt; public override IconUsage? Icon => FontAwesome.Solid.ArrowsAlt;
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING."; public override LocalisableString Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) };

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "WG"; public override string Acronym => "WG";
public override IconUsage? Icon => FontAwesome.Solid.Certificate; public override IconUsage? Icon => FontAwesome.Solid.Certificate;
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "They just won't stay still..."; public override LocalisableString Description => "They just won't stay still...";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel) };

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
connectionPool = new DrawablePool<FollowPointConnection>(1, 200), connectionPool = new DrawablePool<FollowPointConnection>(10, 200),
pointPool = new DrawablePool<FollowPoint>(50, 1000) pointPool = new DrawablePool<FollowPoint>(50, 1000)
}; };
} }

View File

@ -32,6 +32,7 @@ using osu.Game.Skinning;
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Osu.Edit.Setup; using osu.Game.Rulesets.Osu.Edit.Setup;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.Skinning.Legacy;
@ -253,7 +254,7 @@ namespace osu.Game.Rulesets.Osu
}; };
} }
public override string GetDisplayNameForHitResult(HitResult result) public override LocalisableString GetDisplayNameForHitResult(HitResult result)
{ {
switch (result) switch (result)
{ {

View File

@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private bool rotationTransferred; private bool rotationTransferred;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private GameplayClock gameplayClock { get; set; } private IGameplayClock gameplayClock { get; set; }
protected override void Update() protected override void Update()
{ {

View File

@ -9,9 +9,9 @@ using System.Runtime.InteropServices;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Batches;
using osu.Framework.Graphics.OpenGL.Vertices;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input; using osu.Framework.Input;
@ -68,8 +68,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ShaderManager shaders) private void load(IRenderer renderer, ShaderManager shaders)
{ {
texture ??= renderer.WhitePixel;
shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE); shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE);
} }
@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
resetTime(); resetTime();
} }
private Texture texture = Texture.WhitePixel; private Texture texture;
public Texture Texture public Texture Texture
{ {
@ -222,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private Vector2 size; private Vector2 size;
private Vector2 originPosition; private Vector2 originPosition;
private readonly QuadBatch<TexturedTrailVertex> vertexBatch = new QuadBatch<TexturedTrailVertex>(max_sprites, 1); private IVertexBatch<TexturedTrailVertex> vertexBatch;
public TrailDrawNode(CursorTrail source) public TrailDrawNode(CursorTrail source)
: base(source) : base(source)
@ -254,15 +255,17 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Source.parts.CopyTo(parts, 0); Source.parts.CopyTo(parts, 0);
} }
public override void Draw(Action<TexturedVertex2D> vertexAction) public override void Draw(IRenderer renderer)
{ {
base.Draw(vertexAction); base.Draw(renderer);
vertexBatch ??= renderer.CreateQuadBatch<TexturedTrailVertex>(max_sprites, 1);
shader.Bind(); shader.Bind();
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time); shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent); shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent);
texture.TextureGL.Bind(); texture.Bind();
RectangleF textureRect = texture.GetTextureRect(); RectangleF textureRect = texture.GetTextureRect();
@ -319,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
vertexBatch.Dispose(); vertexBatch?.Dispose();
} }
} }

View File

@ -11,9 +11,11 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
@ -112,21 +114,36 @@ namespace osu.Game.Rulesets.Osu.UI
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config) private void load(OsuRulesetConfigManager config, IBeatmap beatmap)
{ {
config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
RegisterPool<HitCircle, DrawableHitCircle>(10, 100); var osuBeatmap = (OsuBeatmap)beatmap;
RegisterPool<Slider, DrawableSlider>(10, 100); RegisterPool<HitCircle, DrawableHitCircle>(20, 100);
RegisterPool<SliderHeadCircle, DrawableSliderHead>(10, 100);
RegisterPool<SliderTailCircle, DrawableSliderTail>(10, 100); // handle edge cases where a beatmap has a slider with many repeats.
RegisterPool<SliderTick, DrawableSliderTick>(10, 100); int maxRepeatsOnOneSlider = 0;
RegisterPool<SliderRepeat, DrawableSliderRepeat>(5, 50); int maxTicksOnOneSlider = 0;
if (osuBeatmap != null)
{
foreach (var slider in osuBeatmap.HitObjects.OfType<Slider>())
{
maxRepeatsOnOneSlider = Math.Max(maxRepeatsOnOneSlider, slider.RepeatCount);
maxTicksOnOneSlider = Math.Max(maxTicksOnOneSlider, slider.NestedHitObjects.OfType<SliderTick>().Count());
}
}
RegisterPool<Slider, DrawableSlider>(20, 100);
RegisterPool<SliderHeadCircle, DrawableSliderHead>(20, 100);
RegisterPool<SliderTailCircle, DrawableSliderTail>(20, 100);
RegisterPool<SliderTick, DrawableSliderTick>(Math.Max(maxTicksOnOneSlider, 20), Math.Max(maxTicksOnOneSlider, 200));
RegisterPool<SliderRepeat, DrawableSliderRepeat>(Math.Max(maxRepeatsOnOneSlider, 20), Math.Max(maxRepeatsOnOneSlider, 200));
RegisterPool<Spinner, DrawableSpinner>(2, 20); RegisterPool<Spinner, DrawableSpinner>(2, 20);
RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 100); RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 200);
RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 100); RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 200);
} }
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject);
@ -173,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly Action<DrawableOsuJudgement> onLoaded; private readonly Action<DrawableOsuJudgement> onLoaded;
public DrawableJudgementPool(HitResult result, Action<DrawableOsuJudgement> onLoaded) public DrawableJudgementPool(HitResult result, Action<DrawableOsuJudgement> onLoaded)
: base(10) : base(20)
{ {
this.result = result; this.result = result;
this.onLoaded = onLoaded; this.onLoaded = onLoaded;

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osuTK; using osuTK;
@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI
public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null; public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null;
protected override string Message => "Click the orange cursor to resume"; protected override LocalisableString Message => "Click the orange cursor to resume";
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()

Some files were not shown because too many files have changed in this diff Show More