1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-19 08:39:54 +08:00

Merge branch 'master' into pp-dev

This commit is contained in:
Dan Balasescu
2025-09-07 19:22:08 +09:00
Unverified
532 changed files with 16688 additions and 4603 deletions
+7 -2
View File
@@ -131,7 +131,7 @@ jobs:
build-only-ios:
name: Build only (iOS)
runs-on: macos-latest
runs-on: macos-15
timeout-minutes: 60
steps:
- name: Checkout
@@ -143,7 +143,12 @@ jobs:
dotnet-version: "8.0.x"
- name: Install .NET Workloads
run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
run: dotnet workload install ios
# https://github.com/dotnet/macios/issues/19157
# https://github.com/actions/runner-images/issues/12758
- name: Use Xcode 16.4
run: sudo xcode-select -switch /Applications/Xcode_16.4.app
- name: Build
run: dotnet build -c Debug osu.iOS.slnf
+1 -1
View File
@@ -50,7 +50,7 @@
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<Company>ppy Pty Ltd</Company>
<Copyright>Copyright (c) 2024 ppy Pty Ltd</Copyright>
<Copyright>Copyright (c) 2025 ppy Pty Ltd</Copyright>
<PackageTags>osu game</PackageTags>
</PropertyGroup>
</Project>
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2024 ppy Pty Ltd <contact@ppy.sh>.
Copyright (c) 2025 ppy Pty Ltd <contact@ppy.sh>.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+1 -1
View File
@@ -8,7 +8,7 @@
<PackageProjectUrl>https://github.com/ppy/osu/blob/master/Templates</PackageProjectUrl>
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<copyright>Copyright (c) 2024 ppy Pty Ltd</copyright>
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
<Description>Templates to use when creating a ruleset for consumption in osu!.</Description>
<PackageTags>dotnet-new;templates;osu</PackageTags>
<TargetFramework>netstandard2.1</TargetFramework>
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.704.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.903.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
+11 -37
View File
@@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using Android.App;
using Android.Content.PM;
using Microsoft.Maui.Devices;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Platform;
using osu.Game;
@@ -21,58 +23,30 @@ namespace osu.Android
[Cached]
private readonly OsuGameActivity gameActivity;
private readonly PackageInfo packageInfo;
public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
public OsuGameAndroid(OsuGameActivity activity)
: base(null)
{
gameActivity = activity;
packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
}
public override Version AssemblyVersion
public override string Version
{
get
{
var packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
if (!IsDeployedBuild)
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
try
{
// We store the osu! build number in the "VersionCode" field to better support google play releases.
// If we were to use the main build number, it would require a new submission each time (similar to TestFlight).
// In order to do this, we should split it up and pad the numbers to still ensure sequential increase over time.
//
// We also need to be aware that older SDK versions store this as a 32bit int.
//
// Basic conversion format (as done in Fastfile): 2020.606.0 -> 202006060
// https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated
string versionName;
if (OperatingSystem.IsAndroidVersionAtLeast(28))
{
versionName = packageInfo.LongVersionCode.ToString();
// ensure we only read the trailing portion of long (the part we are interested in).
versionName = versionName.Substring(versionName.Length - 9);
}
else
{
#pragma warning disable CS0618 // Type or member is obsolete
// this is required else older SDKs will report missing method exception.
versionName = packageInfo.VersionCode.ToString();
#pragma warning restore CS0618 // Type or member is obsolete
}
// undo play store version garbling (as mentioned above).
return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
}
catch
{
}
return new Version(packageInfo.VersionName.AsNonNull());
return packageInfo.VersionName.AsNonNull();
}
}
public override Version AssemblyVersion => new Version(packageInfo.VersionName.AsNonNull().Split('-').First());
protected override void LoadComplete()
{
base.LoadComplete();
+1 -1
View File
@@ -123,7 +123,7 @@ namespace osu.Desktop
public override bool RestartAppWhenExited()
{
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
Task.Run(() => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId)).FireAndForget();
return true;
}
+15 -2
View File
@@ -36,7 +36,7 @@ namespace osu.Desktop
// IMPORTANT DON'T IGNORE: For general sanity, velopack's setup needs to run before anything else.
// This has bitten us in the rear before (bricked updater), and although the underlying issue from
// last time has been fixed, let's not tempt fate.
setupVelopack();
setupVelopack(args);
if (OperatingSystem.IsWindows())
{
@@ -174,8 +174,21 @@ namespace osu.Desktop
return false;
}
private static void setupVelopack()
private static void setupVelopack(string[] args)
{
// Arguments being present indicate the user is either starting the game in a special (aka tournament) mode,
// or is running with pending imports via file association or otherwise.
//
// In both these scenarios, we'd hope the game does not attempt to update.
//
// Special consideration for velopack startup arguments, which must be handled during update.
// See https://docs.velopack.io/integrating/hooks#command-line-hooks.
if (args.Length > 0 && !args[0].StartsWith("--velo", StringComparison.Ordinal))
{
Logger.Log("Handling arguments, skipping velopack setup.");
return;
}
if (OsuGameDesktop.IsPackageManaged)
{
Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup.");
+31 -20
View File
@@ -53,33 +53,44 @@ namespace osu.Desktop.Updater
return false;
}
IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon);
Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions
try
{
AllowVersionDowngrade = true
});
IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon);
Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions
{
AllowVersionDowngrade = true
});
UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested)
if (cancellationToken.IsCancellationRequested)
{
log("Update check cancelled");
scheduleNextUpdateCheck();
return true;
}
if (update == null)
{
// No update is available.
log("No update found");
scheduleNextUpdateCheck();
return false;
}
// Download update in the background while notifying awaiters of the update being available.
log($"New update available: {update.TargetFullRelease.Version}");
downloadUpdate(updateManager, update, cancellationToken);
return true;
}
catch (Exception e)
{
log("Update check cancelled");
log($"Update check failed with error ({e.Message})");
// we shouldn't crash on a web failure. or any failure for the matter.
scheduleNextUpdateCheck();
return true;
}
if (update == null)
{
// No update is available.
log("No update found");
scheduleNextUpdateCheck();
return false;
}
// Download update in the background while notifying awaiters of the update being available.
log($"New update available: {update.TargetFullRelease.Version}");
downloadUpdate(updateManager, update, cancellationToken);
return true;
}
private void downloadUpdate(Velopack.UpdateManager updateManager, UpdateInfo update, CancellationToken cancellationToken) => Task.Run(async () =>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.
+1 -1
View File
@@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
<releaseNotes>testing</releaseNotes>
<copyright>Copyright (c) 2024 ppy Pty Ltd</copyright>
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
<language>en-AU</language>
</metadata>
<files>
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
namespace osu.Game.Rulesets.Catch.Tests
{
@@ -21,8 +22,9 @@ namespace osu.Game.Rulesets.Catch.Tests
{
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, []);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
}
@@ -32,8 +34,9 @@ namespace osu.Game.Rulesets.Catch.Tests
{
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty();
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, [new CatchModHalfTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
}
@@ -43,8 +46,9 @@ namespace osu.Game.Rulesets.Catch.Tests
{
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty();
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, [new CatchModDoubleTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
}
@@ -0,0 +1,21 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Mods
{
public partial class TestSceneCatchModMovingFast : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[Test]
public void TestMovingFast() => CreateModTest(new ModTestData
{
Mod = new CatchModMovingFast(),
PassCondition = () => true
});
}
}
+35 -2
View File
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -11,6 +12,7 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Catch.Edit;
@@ -25,6 +27,7 @@ using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy;
@@ -33,6 +36,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Rulesets.Catch
@@ -150,6 +154,7 @@ namespace osu.Game.Rulesets.Catch
new CatchModFloatingFruits(),
new CatchModMuted(),
new CatchModNoScope(),
new CatchModMovingFast(),
};
case ModType.System:
@@ -265,9 +270,10 @@ namespace osu.Game.Rulesets.Catch
}
/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
BeatmapDifficulty adjustedDifficulty = base.GetAdjustedDisplayDifficulty(beatmapInfo, mods);
double rate = ModUtils.CalculateRateWithMods(mods);
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
preempt /= rate;
@@ -276,6 +282,33 @@ namespace osu.Game.Rulesets.Catch
return adjustedDifficulty;
}
public override IEnumerable<RulesetBeatmapAttribute> GetBeatmapAttributesForDisplay(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
{
var originalDifficulty = beatmapInfo.Difficulty;
var effectiveDifficulty = GetAdjustedDisplayDifficulty(beatmapInfo, mods);
yield return new RulesetBeatmapAttribute(SongSelectStrings.CircleSize, @"CS", originalDifficulty.CircleSize, effectiveDifficulty.CircleSize, 10)
{
Description = "Affects the size of fruits.",
AdditionalMetrics =
[
new RulesetBeatmapAttribute.AdditionalMetric("Hit circle radius", (CatchHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(effectiveDifficulty.CircleSize)).ToLocalisableString("0.#"))
]
};
yield return new RulesetBeatmapAttribute(SongSelectStrings.ApproachRate, @"AR", originalDifficulty.ApproachRate, effectiveDifficulty.ApproachRate, 10)
{
Description = "Affects how early fruits fade in on the screen.",
AdditionalMetrics =
[
new RulesetBeatmapAttribute.AdditionalMetric("Fade-in time", LocalisableString.Interpolate($@"{IBeatmapDifficultyInfo.DifficultyRange(effectiveDifficulty.ApproachRate, CatchHitObject.PREEMPT_RANGE):#,0.##} ms"))
]
};
yield return new RulesetBeatmapAttribute(SongSelectStrings.HPDrain, @"HP", originalDifficulty.DrainRate, effectiveDifficulty.DrainRate, 10)
{
Description = "Affects the harshness of health drain and the health penalties for missing."
};
}
public override bool EditorShowScrollSpeed => false;
}
}
@@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Catch.Edit
new CheckBananaShowerGap(),
new CheckConcurrentObjects(),
// Spread
new CheckCatchLowestDiffDrainTime(),
// Settings
new CheckCatchAbnormalDifficultySettings(),
};
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var hitObjects = context.Beatmap.HitObjects;
var hitObjects = context.CurrentDifficulty.Playable.HitObjects;
(int expectedStartDelta, int expectedEndDelta) = spinner_delta_threshold[context.InterpretedDifficulty];
for (int i = 0; i < hitObjects.Count - 1; ++i)
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var diff = context.Beatmap.Difficulty;
var diff = context.CurrentDifficulty.Playable.Difficulty;
Issue? issue;
if (HasMoreThanOneDecimalPlace("Approach rate", diff.ApproachRate, out issue))
@@ -0,0 +1,21 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
namespace osu.Game.Rulesets.Catch.Edit.Checks
{
public class CheckCatchLowestDiffDrainTime : CheckLowestDiffDrainTime
{
protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds()
{
// See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21catch#general
yield return (DifficultyRating.Hard, new TimeSpan(0, 2, 30).TotalMilliseconds, "Platter");
yield return (DifficultyRating.Insane, new TimeSpan(0, 3, 15).TotalMilliseconds, "Rain");
yield return (DifficultyRating.Expert, new TimeSpan(0, 4, 0).TotalMilliseconds, "Overdose");
}
}
}
@@ -1,7 +1,9 @@
// 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 System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods;
@@ -10,6 +12,8 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModAutoplay : ModAutoplay
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(CatchModMovingFast) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
}
@@ -1,7 +1,9 @@
// 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 System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
@@ -11,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModCinema : ModCinema<CatchHitObject>
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(CatchModMovingFast) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
}
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 325;
public override float DefaultFlashlightSize => 203.125f;
protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield);
@@ -3,6 +3,7 @@
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public override string Acronym => "FF";
public override LocalisableString Description => "The fruits are... floating?";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon => FontAwesome.Solid.Cloud;
public override IconUsage? Icon => OsuIcon.ModFloatingFruits;
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
{
@@ -0,0 +1,82 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Rulesets.Catch.Mods
{
public partial class CatchModMovingFast : Mod, IApplicableToDrawableRuleset<CatchHitObject>, IApplicableToPlayer
{
public override string Name => "Moving Fast";
public override string Acronym => "MF";
public override LocalisableString Description => "Dashing by default, slow down!";
public override ModType Type => ModType.Fun;
public override double ScoreMultiplier => 1;
public override IconUsage? Icon => OsuIcon.ModMovingFast;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
private DrawableCatchRuleset drawableRuleset = null!;
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
{
this.drawableRuleset = (DrawableCatchRuleset)drawableRuleset;
}
public void ApplyToPlayer(Player player)
{
if (!drawableRuleset.HasReplayLoaded.Value)
{
var catchPlayfield = (CatchPlayfield)drawableRuleset.Playfield;
catchPlayfield.Catcher.Dashing = true;
catchPlayfield.CatcherArea.Add(new InvertDashInputHelper(catchPlayfield.CatcherArea));
}
}
private partial class InvertDashInputHelper : Drawable, IKeyBindingHandler<CatchAction>
{
private readonly CatcherArea catcherArea;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public InvertDashInputHelper(CatcherArea catcherArea)
{
this.catcherArea = catcherArea;
RelativeSizeAxes = Axes.Both;
}
public bool OnPressed(KeyBindingPressEvent<CatchAction> e)
{
switch (e.Action)
{
case CatchAction.MoveLeft or CatchAction.MoveRight:
break;
case CatchAction.Dash:
catcherArea.Catcher.Dashing = false;
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<CatchAction> e)
{
if (e.Action == CatchAction.Dash)
catcherArea.Catcher.Dashing = true;
}
}
}
}
@@ -1,6 +1,8 @@
// 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.Linq;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
@@ -19,6 +21,8 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override LocalisableString Description => @"Use the mouse to control the catcher.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(CatchModMovingFast) }).ToArray();
private DrawableCatchRuleset drawableRuleset = null!;
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
@@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_RANGE);
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize);
}
@@ -203,6 +203,8 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary>
public const double PREEMPT_MAX = 1800;
public static readonly DifficultyRange PREEMPT_RANGE = new DifficultyRange(PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);
/// <summary>
/// The Y position of the hit object is not used in the normal osu!catch gameplay.
/// It is preserved to maximize the backward compatibility with the legacy editor, in which the mappers use the Y position to organize the patterns.
@@ -55,6 +55,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks
});
}
[Test]
public void TestHoldNotesAlmostConcurrentOnSameColumn()
{
assertAlmostConcurrentSame(new List<HitObject>
{
createHoldNote(startTime: 100, endTime: 400.75d, column: 1),
createHoldNote(startTime: 408, endTime: 700.75d, column: 1)
});
}
private void assertOk(List<HitObject> hitobjects)
{
Assert.That(check.Run(getContext(hitobjects)), Is.Empty);
@@ -65,7 +75,17 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks
var issues = check.Run(getContext(hitobjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentSame));
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrent));
Assert.That(issues.All(issue => issue.ToString().Contains("s are concurrent here")));
}
private void assertAlmostConcurrentSame(List<HitObject> hitobjects)
{
var issues = check.Run(getContext(hitobjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateAlmostConcurrent));
Assert.That(issues.All(issue => issue.ToString().Contains("s are less than 10ms apart")));
}
private BeatmapVerifierContext getContext(List<HitObject> hitobjects)
@@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase("convert-samples")]
[TestCase("mania-samples")]
[TestCase("mania-slider")] // e.g. second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407
[TestCase("slider-convert-samples")]
public void Test(string name) => base.Test(name);
@@ -32,6 +33,7 @@ namespace osu.Game.Rulesets.Mania.Tests
StartTime = hitObject.StartTime,
EndTime = hitObject.GetEndTime(),
Column = ((ManiaHitObject)hitObject).Column,
PlaySlidingSamples = hitObject is HoldNote holdNote && holdNote.PlaySlidingSamples,
Samples = getSampleNames(hitObject.Samples),
NodeSamples = getNodeSampleNames((hitObject as HoldNote)?.NodeSamples)
};
@@ -57,12 +59,14 @@ namespace osu.Game.Rulesets.Mania.Tests
public double StartTime;
public double EndTime;
public int Column;
public bool PlaySlidingSamples;
public IList<string> Samples;
public IList<IList<string>> NodeSamples;
public bool Equals(SampleConvertValue other)
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
&& PlaySlidingSamples == other.PlaySlidingSamples
&& samplesEqual(Samples, other.Samples)
&& nodeSamplesEqual(NodeSamples, other.NodeSamples);
@@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}
[TestCase]
public void TestFilterIntersection()
public void TestKeysFilterIntersection()
{
var criteria = new ManiaFilterCriteria();
criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4");
@@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}
[TestCase]
public void TestInvalidFilters()
public void TestInvalidKeysFilters()
{
var criteria = new ManiaFilterCriteria();
@@ -183,5 +183,132 @@ namespace osu.Game.Rulesets.Mania.Tests
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text"));
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6"));
}
[TestCase]
public void TestLnsEqual()
{
var criteria = new ManiaFilterCriteria();
var filterCriteria = new FilterCriteria
{
Ruleset = new ManiaRuleset().RulesetInfo
};
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0");
BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 0,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo1, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0");
BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo2, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100");
BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 100
};
Assert.True(criteria.Matches(beatmapInfo3, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "1");
BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo4, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1");
BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 1000,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo5, filterCriteria));
}
[TestCase]
public void TestLnsGreaterOrEqual()
{
var criteria = new ManiaFilterCriteria();
var filterCriteria = new FilterCriteria
{
Ruleset = new ManiaRuleset().RulesetInfo
};
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0");
BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 0,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo1, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0");
BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo2, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100");
BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 100
};
Assert.True(criteria.Matches(beatmapInfo3, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1");
BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo4, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0.1");
BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 1000,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo5, filterCriteria));
}
[TestCase]
public void TestLnsNotManiaRuleset()
{
var criteria = new ManiaFilterCriteria();
var filterCriteria = new FilterCriteria
{
Ruleset = new ManiaRuleset().RulesetInfo
};
criteria.TryParseCustomKeywordCriteria("lns", Operator.LessOrEqual, "100");
BeatmapInfo beatmapInfo = new BeatmapInfo
{
TotalObjectCount = 100,
EndTimeObjectCount = 50
};
Assert.False(criteria.Matches(beatmapInfo, filterCriteria));
}
[TestCase]
public void TestInvalidLnsFilters()
{
var criteria = new ManiaFilterCriteria();
Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "some text"));
Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1some text"));
}
}
}
@@ -5,6 +5,7 @@
"StartTime": 1000.0,
"EndTime": 2750.0,
"Column": 1,
"PlaySlidingSamples": true,
"NodeSamples": [
["Gameplay/normal-hitnormal"],
["Gameplay/soft-hitnormal"],
@@ -15,6 +16,7 @@
"StartTime": 1875.0,
"EndTime": 2750.0,
"Column": 0,
"PlaySlidingSamples": true,
"NodeSamples": [
["Gameplay/soft-hitnormal"],
["Gameplay/drum-hitnormal"]
@@ -5,6 +5,7 @@
"StartTime": 500.0,
"EndTime": 1500.0,
"Column": 0,
"PlaySlidingSamples": false,
"NodeSamples": [
["Gameplay/normal-hitnormal"],
[]
@@ -17,6 +18,7 @@
"StartTime": 2000.0,
"EndTime": 3000.0,
"Column": 2,
"PlaySlidingSamples": false,
"NodeSamples": [
["Gameplay/drum-hitnormal"],
[]
@@ -0,0 +1,18 @@
{
"Mappings": [{
"StartTime": 500.0,
"Objects": [{
"StartTime": 500.0,
"EndTime": 2500,
"Column": 2,
"PlaySlidingSamples": true,
"NodeSamples": [
["Gameplay/soft-hitnormal"],
["Gameplay/soft-hitnormal"],
["Gameplay/soft-hitnormal"],
["Gameplay/soft-hitnormal"]
],
"Samples": ["Gameplay/soft-hitnormal"]
}]
}]
}
@@ -0,0 +1,29 @@
osu file format v5
[General]
StackLeniency: 0.7
Mode: 3
[Difficulty]
HPDrainRate:2
CircleSize:5
OverallDifficulty:2
SliderMultiplier:1
SliderTickRate:2
[Events]
//Background and Video events
//Break Periods
//Storyboard Layer 0 (Background)
//Storyboard Layer 1 (Failing)
//Storyboard Layer 2 (Passing)
//Storyboard Layer 3 (Foreground)
//Storyboard Sound Samples
//Background Colour Transformations
3,100,163,162,255
[TimingPoints]
355,476.190476190476,4,2,1,60,1,0
[HitObjects]
256,352,500,2,0,L|256:208,3,140
@@ -203,7 +203,7 @@ namespace osu.Game.Rulesets.Mania.Tests
private void toggleTouchControls(bool enabled)
{
var maniaConfig = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(CreatePlayerRuleset())!;
maniaConfig.SetValue(ManiaRulesetSetting.MobileLayout, enabled ? ManiaMobileLayout.LandscapeWithOverlay : ManiaMobileLayout.Portrait);
maniaConfig.SetValue(ManiaRulesetSetting.TouchOverlay, enabled);
}
private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType<ManiaTouchInputArea>().SingleOrDefault();
@@ -47,8 +47,6 @@ namespace osu.Game.Rulesets.Mania.Tests
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
}
};
drawableRuleset.AllowBackwardsSeeks = true;
});
AddStep("retrieve config bindable", () =>
{
@@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
}
}
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty, IReadOnlyList<Mod>? mods = null)
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty, IReadOnlyCollection<Mod>? mods = null)
{
var converter = new ManiaBeatmapConverter(null, difficulty, new ManiaRuleset());
@@ -3,8 +3,10 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Utils;
@@ -30,12 +32,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (HitObject is IHasDuration endTimeData)
{
// despite the beatmap originally being made for mania, if the object is parsed as a slider rather than a hold, sliding samples should still be played.
// this is seemingly only possible to achieve by modifying the .osu file directly, but online beatmaps that do that exist
// (see second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407)
bool playSlidingSamples = (HitObject is IHasLegacyHitObjectType hasType && hasType.LegacyType == LegacyHitObjectType.Slider) || HitObject is IHasPath;
pattern.Add(new HoldNote
{
StartTime = HitObject.StartTime,
Duration = endTimeData.Duration,
Column = column,
Samples = HitObject.Samples,
PlaySlidingSamples = playSlidingSamples,
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject)
});
}
@@ -521,6 +521,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
Duration = endTime - startTime,
Column = column,
Samples = HitObject.Samples,
PlaySlidingSamples = true,
NodeSamples = nodeSamplesAt(startTime)
};
}
@@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
public ManiaRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null)
: base(settings, ruleset, variant)
{
Migrate();
}
protected override void InitialiseDefaults()
@@ -24,6 +25,20 @@ namespace osu.Game.Rulesets.Mania.Configuration
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
SetDefault(ManiaRulesetSetting.MobileLayout, ManiaMobileLayout.Portrait);
SetDefault(ManiaRulesetSetting.TouchOverlay, false);
}
public void Migrate()
{
var mobileLayout = GetBindable<ManiaMobileLayout>(ManiaRulesetSetting.MobileLayout);
#pragma warning disable CS0618 // Type or member is obsolete
if (mobileLayout.Value == ManiaMobileLayout.LandscapeWithOverlay)
#pragma warning restore CS0618 // Type or member is obsolete
{
mobileLayout.Value = ManiaMobileLayout.Landscape;
SetValue(ManiaRulesetSetting.TouchOverlay, true);
}
}
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
@@ -44,5 +59,6 @@ namespace osu.Game.Rulesets.Mania.Configuration
ScrollDirection,
TimingBasedNoteColouring,
MobileLayout,
TouchOverlay,
}
}
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Checks
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var diff = context.Beatmap.Difficulty;
var diff = context.CurrentDifficulty.Playable.Difficulty;
if (diff.CircleSize < 4)
{
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Checks
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var diff = context.Beatmap.Difficulty;
var diff = context.CurrentDifficulty.Playable.Difficulty;
Issue? issue;
if (HasMoreThanOneDecimalPlace("Overall difficulty", diff.OverallDifficulty, out issue))
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Checks
{
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var hitObjects = context.Beatmap.HitObjects;
var hitObjects = context.CurrentDifficulty.Playable.HitObjects;
for (int i = 0; i < hitObjects.Count - 1; ++i)
{
@@ -28,14 +28,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Checks
continue;
// Two hitobjects cannot be concurrent without also being concurrent with all objects in between.
// So if the next object is not concurrent, then we know no future objects will be either.
if (!AreConcurrent(hitobject, nextHitobject))
// So if the next object is not concurrent or almost concurrent, then we know no future objects will be either.
if (!AreConcurrent(hitobject, nextHitobject) && !AreAlmostConcurrent(hitobject, nextHitobject))
break;
if (hitobject.GetType() == nextHitobject.GetType())
yield return new IssueTemplateConcurrentSame(this).Create(hitobject, nextHitobject);
else
yield return new IssueTemplateConcurrentDifferent(this).Create(hitobject, nextHitobject);
if (AreConcurrent(hitobject, nextHitobject))
{
yield return new IssueTemplateConcurrent(this).Create(hitobject, nextHitobject);
}
else if (AreAlmostConcurrent(hitobject, nextHitobject))
{
yield return new IssueTemplateAlmostConcurrent(this).Create(hitobject, nextHitobject);
}
}
}
}
@@ -0,0 +1,21 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
namespace osu.Game.Rulesets.Mania.Edit.Checks
{
public class CheckManiaLowestDiffDrainTime : CheckLowestDiffDrainTime
{
protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds()
{
// See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21mania#rules
yield return (DifficultyRating.Hard, new TimeSpan(0, 2, 30).TotalMilliseconds, "Hard");
yield return (DifficultyRating.Insane, new TimeSpan(0, 2, 45).TotalMilliseconds, "Insane");
yield return (DifficultyRating.Expert, new TimeSpan(0, 3, 30).TotalMilliseconds, "Expert");
}
}
}
@@ -16,6 +16,9 @@ namespace osu.Game.Rulesets.Mania.Edit
// Compose
new CheckManiaConcurrentObjects(),
// Spread
new CheckManiaLowestDiffDrainTime(),
// Settings
new CheckKeyCount(),
new CheckManiaAbnormalDifficultySettings(),
+23 -1
View File
@@ -1,6 +1,7 @@
// 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 System.Linq;
using osu.Framework.Bindables;
@@ -19,12 +20,16 @@ namespace osu.Game.Rulesets.Mania
public class ManiaFilterCriteria : IRulesetFilterCriteria
{
private readonly HashSet<int> includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet();
private FilterCriteria.OptionalRange<float> longNotePercentage;
public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria)
{
int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods);
return includedKeyCounts.Contains(keyCount);
bool keyCountMatch = includedKeyCounts.Contains(keyCount);
bool longNotePercentageMatch = !longNotePercentage.HasFilter || (!isConvertedBeatmap(beatmapInfo) && longNotePercentage.IsInRange(calculateLongNotePercentage(beatmapInfo)));
return keyCountMatch && longNotePercentageMatch;
}
public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues)
@@ -84,6 +89,10 @@ namespace osu.Game.Rulesets.Mania
return false;
}
}
case "ln":
case "lns":
return FilterQueryParser.TryUpdateCriteriaRange(ref longNotePercentage, op, strValues);
}
return false;
@@ -103,5 +112,18 @@ namespace osu.Game.Rulesets.Mania
return false;
}
private static bool isConvertedBeatmap(BeatmapInfo beatmapInfo)
{
return !beatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
}
private static float calculateLongNotePercentage(BeatmapInfo beatmapInfo)
{
int holdNotes = beatmapInfo.EndTimeObjectCount;
int totalNotes = Math.Max(1, beatmapInfo.TotalObjectCount);
return holdNotes / (float)totalNotes * 100;
}
}
}
+8 -3
View File
@@ -1,20 +1,25 @@
// 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 osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mania.Configuration;
namespace osu.Game.Rulesets.Mania
{
public enum ManiaMobileLayout
{
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.PortraitExpandedColumns))]
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.Portrait))]
Portrait,
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.LandscapeExpandedColumns))]
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.Landscape))]
Landscape,
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.LandscapeTouchOverlay))]
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.LandscapeExpandedColumns))]
LandscapeExpandedColumns,
[Obsolete($"Use {nameof(ManiaRulesetSetting.TouchOverlay)} instead.")] // todo: can be removed 20260211
LandscapeWithOverlay,
}
}
+66
View File
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
@@ -12,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Difficulty;
@@ -414,6 +416,70 @@ namespace osu.Game.Rulesets.Mania
}), true)
};
/// <seealso cref="ManiaHitWindows"/>
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
{
BeatmapDifficulty adjustedDifficulty = base.GetAdjustedDisplayDifficulty(beatmapInfo, mods);
// notably, in mania, hit windows are designed to be independent of track playback rate (see `ManiaHitWindows.SpeedMultiplier`).
// *however*, to not make matters *too* simple, mania Hard Rock and Easy differ from all other rulesets
// in that they apply multipliers *to hit window durations directly* rather than to the Overall Difficulty attribute itself.
// because the duration of hit window durations as a function of OD is not a linear function,
// this means that multiplying the OD is *not* the same thing as multiplying the hit window duration.
// in fact, the second operation is *much* harsher and will produce values much farther outside of normal operating range
// (even negative in the case of Easy).
// stable handles this wrong on song select and just assumes that it can handle mania EZ / HR the same way as all other rulesets.
double perfectHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, ManiaHitWindows.PERFECT_WINDOW_RANGE);
if (mods.Any(m => m is ManiaModHardRock))
perfectHitWindow /= ManiaModHardRock.HIT_WINDOW_DIFFICULTY_MULTIPLIER;
else if (mods.Any(m => m is ManiaModEasy))
perfectHitWindow /= ManiaModEasy.HIT_WINDOW_DIFFICULTY_MULTIPLIER;
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(perfectHitWindow, ManiaHitWindows.PERFECT_WINDOW_RANGE);
adjustedDifficulty.CircleSize = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods);
return adjustedDifficulty;
}
public override IEnumerable<RulesetBeatmapAttribute> GetBeatmapAttributesForDisplay(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
{
// a special touch-up of key count is required to the original difficulty, since key conversion mods are not `IApplicableToDifficulty`
var originalDifficulty = new BeatmapDifficulty(beatmapInfo.Difficulty)
{
CircleSize = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), [])
};
var adjustedDifficulty = GetAdjustedDisplayDifficulty(beatmapInfo, mods);
var colours = new OsuColour();
yield return new RulesetBeatmapAttribute(SongSelectStrings.KeyCount, @"KC", originalDifficulty.CircleSize, adjustedDifficulty.CircleSize, 18)
{
Description = "Affects the number of key columns on the playfield."
};
var hitWindows = new ManiaHitWindows();
hitWindows.SetDifficulty(adjustedDifficulty.OverallDifficulty);
hitWindows.IsConvert = !beatmapInfo.Ruleset.Equals(RulesetInfo);
hitWindows.ClassicModActive = mods.Any(m => m is ManiaModClassic);
yield return new RulesetBeatmapAttribute(SongSelectStrings.Accuracy, @"OD", originalDifficulty.OverallDifficulty, adjustedDifficulty.OverallDifficulty, 10)
{
Description = "Affects timing requirements for notes.",
AdditionalMetrics = hitWindows.GetAllAvailableWindows()
.Reverse()
.Select(window => new RulesetBeatmapAttribute.AdditionalMetric(
$"{window.result.GetDescription().ToUpperInvariant()} hit window",
LocalisableString.Interpolate($@"±{hitWindows.WindowFor(window.result):0.##} ms"),
colours.ForHitResult(window.result)
)).ToArray()
};
yield return new RulesetBeatmapAttribute(SongSelectStrings.HPDrain, @"HP", originalDifficulty.DrainRate, adjustedDifficulty.DrainRate, 10)
{
Description = "Affects the harshness of health drain and the health penalties for missing."
};
}
public override IRulesetFilterCriteria CreateRulesetFilterCriteria()
{
return new ManiaFilterCriteria();
@@ -1,6 +1,8 @@
// 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.Linq;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -48,12 +50,21 @@ namespace osu.Game.Rulesets.Mania
},
};
Add(new SettingsCheckbox
{
LabelText = RulesetSettingsStrings.TouchOverlay,
Current = config.GetBindable<bool>(ManiaRulesetSetting.TouchOverlay)
});
if (RuntimeInfo.IsMobile)
{
Add(new SettingsEnumDropdown<ManiaMobileLayout>
{
LabelText = RulesetSettingsStrings.MobileLayout,
Current = config.GetBindable<ManiaMobileLayout>(ManiaRulesetSetting.MobileLayout),
#pragma warning disable CS0618 // Type or member is obsolete
Items = Enum.GetValues<ManiaMobileLayout>().Where(l => l != ManiaMobileLayout.LandscapeWithOverlay),
#pragma warning restore CS0618 // Type or member is obsolete
});
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Acronym => Name;
public abstract int KeyCount { get; }
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier
public override double ScoreMultiplier => 0.9;
public override bool Ranked => UsesDefaultConfiguration;
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
@@ -4,6 +4,7 @@
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
@@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override LocalisableString Description => "No more tricky speed changes!";
public override IconUsage? Icon => FontAwesome.Solid.Equals;
public override IconUsage? Icon => OsuIcon.ModConstantSpeed;
public override ModType Type => ModType.Conversion;
@@ -4,8 +4,10 @@
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Mods
@@ -14,6 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override string Name => "Cover";
public override string Acronym => "CO";
public override IconUsage? Icon => OsuIcon.ModCover;
public override LocalisableString Description => @"Decrease the playfield's viewing area.";
@@ -1,8 +1,10 @@
// 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.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -13,6 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "Dual Stages";
public override string Acronym => "DS";
public override LocalisableString Description => @"Double the stages, double the fun!";
public override IconUsage? Icon => OsuIcon.ModDualStages;
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1;
+5 -5
View File
@@ -13,19 +13,19 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!";
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1 / 1.4;
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
{
const double multiplier = 1 / 1.4;
switch (hitObject)
{
case Note:
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = multiplier;
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
case HoldNote hold:
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = multiplier;
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = multiplier;
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
}
}
@@ -3,7 +3,9 @@
using System;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Mods
@@ -12,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override string Name => "Fade In";
public override string Acronym => "FI";
public override IconUsage? Icon => OsuIcon.ModFadeIn;
public override LocalisableString Description => @"Keys appear out of nowhere!";
public override double ScoreMultiplier => 1;
public override bool ValidForFreestyleAsRequiredMod => false;
@@ -13,19 +13,19 @@ namespace osu.Game.Rulesets.Mania.Mods
public override double ScoreMultiplier => 1;
public override bool Ranked => false;
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1.4;
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
{
const double multiplier = 1.4;
switch (hitObject)
{
case Note:
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = multiplier;
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
case HoldNote hold:
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = multiplier;
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = multiplier;
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
}
}
@@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
namespace osu.Game.Rulesets.Mania.Mods
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override LocalisableString Description => @"Replaces all hold notes with normal notes.";
public override IconUsage? Icon => FontAwesome.Solid.DotCircle;
public override IconUsage? Icon => OsuIcon.ModHoldOff;
public override ModType Type => ModType.Conversion;
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override LocalisableString Description => "Hold the keys. To the beat.";
public override IconUsage? Icon => FontAwesome.Solid.YinYang;
public override IconUsage? Icon => OsuIcon.ModInvert;
public override ModType Type => ModType.Conversion;
@@ -63,6 +64,7 @@ namespace osu.Game.Rulesets.Mania.Mods
StartTime = locations[i].startTime,
Duration = duration,
NodeSamples = new List<IList<HitSampleInfo>> { locations[i].samples, Array.Empty<HitSampleInfo>() }
// intentionally don't play sliding samples here, it doesn't work in this mod.
});
}
@@ -1,7 +1,9 @@
// 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.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 1;
public override string Name => "One Key";
public override string Acronym => "1K";
public override IconUsage? Icon => OsuIcon.ModOneKey;
public override LocalisableString Description => @"Play with one key.";
public override bool Ranked => false;
}
@@ -1,7 +1,9 @@
// 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.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 10;
public override string Name => "Ten Keys";
public override string Acronym => "10K";
public override IconUsage? Icon => OsuIcon.ModTenKeys;
public override LocalisableString Description => @"Play with ten keys.";
public override bool Ranked => false;
}
@@ -1,7 +1,9 @@
// 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.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 2;
public override string Name => "Two Keys";
public override string Acronym => "2K";
public override IconUsage? Icon => OsuIcon.ModTwoKeys;
public override LocalisableString Description => @"Play with two keys.";
public override bool Ranked => false;
}
@@ -1,7 +1,9 @@
// 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.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 3;
public override string Name => "Three Keys";
public override string Acronym => "3K";
public override IconUsage? Icon => OsuIcon.ModThreeKeys;
public override LocalisableString Description => @"Play with three keys.";
public override bool Ranked => false;
}
@@ -1,7 +1,9 @@
// 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.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 4;
public override string Name => "Four Keys";
public override string Acronym => "4K";
public override IconUsage? Icon => OsuIcon.ModFourKeys;
public override LocalisableString Description => @"Play with four keys.";
}
}
@@ -1,7 +1,9 @@
// 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.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 5;
public override string Name => "Five Keys";
public override string Acronym => "5K";
public override IconUsage? Icon => OsuIcon.ModFiveKeys;
public override LocalisableString Description => @"Play with five keys.";
}
}
@@ -1,7 +1,9 @@
// 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.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 6;
public override string Name => "Six Keys";
public override string Acronym => "6K";
public override IconUsage? Icon => OsuIcon.ModSixKeys;
public override LocalisableString Description => @"Play with six keys.";
}
}
@@ -1,7 +1,9 @@
// 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.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 7;
public override string Name => "Seven Keys";
public override string Acronym => "7K";
public override IconUsage? Icon => OsuIcon.ModSevenKeys;
public override LocalisableString Description => @"Play with seven keys.";
}
}
@@ -1,7 +1,9 @@
// 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.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 8;
public override string Name => "Eight Keys";
public override string Acronym => "8K";
public override IconUsage? Icon => OsuIcon.ModEightKeys;
public override LocalisableString Description => @"Play with eight keys.";
}
}
@@ -1,7 +1,9 @@
// 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.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 9;
public override string Name => "Nine Keys";
public override string Acronym => "9K";
public override IconUsage? Icon => OsuIcon.ModNineKeys;
public override LocalisableString Description => @"Play with nine keys.";
}
}
@@ -4,8 +4,10 @@
using System;
using System.Linq;
using System.Threading;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
@@ -26,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Mods
public override double ScoreMultiplier => 0.9;
public override IconUsage? Icon => OsuIcon.ModNoRelease;
public override ModType Type => ModType.DifficultyReduction;
public override Type[] IncompatibleMods => new[] { typeof(ManiaModHoldOff) };
@@ -80,7 +84,9 @@ namespace osu.Game.Rulesets.Mania.Mods
StartTime = hold.StartTime;
Duration = hold.Duration;
Column = hold.Column;
Samples = hold.Samples;
NodeSamples = hold.NodeSamples;
PlaySlidingSamples = hold.PlaySlidingSamples;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
@@ -355,7 +355,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private void updateSlidingSample(ValueChangedEvent<bool> tracking)
{
if (tracking.NewValue)
if (tracking.NewValue && HitObject.PlaySlidingSamples)
slidingSample?.Play();
else
slidingSample?.Stop();
@@ -86,6 +86,11 @@ namespace osu.Game.Rulesets.Mania.Objects
/// </summary>
public HoldNoteBody Body { get; protected set; }
/// <summary>
/// Whether sliding samples should be played when held.
/// </summary>
public bool PlaySlidingSamples { get; init; }
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
@@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
public class ManiaHitWindows : HitWindows
{
private static readonly DifficultyRange perfect_window_range = new DifficultyRange(22.4D, 19.4D, 13.9D);
public static readonly DifficultyRange PERFECT_WINDOW_RANGE = new DifficultyRange(22.4D, 19.4D, 13.9D);
private static readonly DifficultyRange great_window_range = new DifficultyRange(64, 49, 34);
private static readonly DifficultyRange good_window_range = new DifficultyRange(97, 82, 67);
private static readonly DifficultyRange ok_window_range = new DifficultyRange(127, 112, 97);
@@ -151,7 +151,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
}
else
{
perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, perfect_window_range) * totalMultiplier) + 0.5;
perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, PERFECT_WINDOW_RANGE) * totalMultiplier) + 0.5;
great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, great_window_range) * totalMultiplier) + 0.5;
good = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, good_window_range) * totalMultiplier) + 0.5;
ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, ok_window_range) * totalMultiplier) + 0.5;
+3 -3
View File
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>(Color4.Black);
private IBindable<ManiaMobileLayout> mobilePlayStyle = null!;
private IBindable<bool> touchOverlay = null!;
private float leftColumnSpacing;
private float rightColumnSpacing;
@@ -123,7 +123,7 @@ namespace osu.Game.Rulesets.Mania.UI
RegisterPool<HoldNoteBody, DrawableHoldNoteBody>(10, 50);
if (rulesetConfig != null)
mobilePlayStyle = rulesetConfig.GetBindable<ManiaMobileLayout>(ManiaRulesetSetting.MobileLayout);
touchOverlay = rulesetConfig.GetBindable<bool>(ManiaRulesetSetting.TouchOverlay);
}
private void onSourceChanged()
@@ -214,7 +214,7 @@ namespace osu.Game.Rulesets.Mania.UI
protected override bool OnTouchDown(TouchDownEvent e)
{
// if touch overlay is visible, disallow columns from handling touch directly.
if (mobilePlayStyle.Value == ManiaMobileLayout.LandscapeWithOverlay)
if (touchOverlay.Value)
return false;
maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
+1 -1
View File
@@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Mania.UI
{
float mobileAdjust = 1f;
if (RuntimeInfo.IsMobile && mobileLayout.Value == ManiaMobileLayout.Landscape)
if (RuntimeInfo.IsMobile && mobileLayout.Value == ManiaMobileLayout.LandscapeExpandedColumns)
{
// GridContainer+CellContainer containing this stage (gets split up for dual stages).
Vector2? containingCell = this.FindClosestParent<Stage>()?.Parent?.DrawSize;
@@ -59,6 +59,7 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
private readonly BindableDouble configScrollSpeed = new BindableDouble();
private readonly Bindable<ManiaMobileLayout> mobileLayout = new Bindable<ManiaMobileLayout>();
private readonly Bindable<bool> touchOverlay = new Bindable<bool>();
public double TargetTimeRange { get; protected set; }
@@ -122,24 +123,23 @@ namespace osu.Game.Rulesets.Mania.UI
Config.BindWith(ManiaRulesetSetting.MobileLayout, mobileLayout);
mobileLayout.BindValueChanged(_ => updateMobileLayout(), true);
Config.BindWith(ManiaRulesetSetting.TouchOverlay, touchOverlay);
touchOverlay.BindValueChanged(_ => updateMobileLayout(), true);
}
private ManiaTouchInputArea? touchInputArea;
private void updateMobileLayout()
{
switch (mobileLayout.Value)
if (touchOverlay.Value)
KeyBindingInputManager.Add(touchInputArea = new ManiaTouchInputArea(this));
else
{
case ManiaMobileLayout.LandscapeWithOverlay:
KeyBindingInputManager.Add(touchInputArea = new ManiaTouchInputArea(this));
break;
if (touchInputArea != null)
KeyBindingInputManager.Remove(touchInputArea, true);
default:
if (touchInputArea != null)
KeyBindingInputManager.Remove(touchInputArea, true);
touchInputArea = null;
break;
touchInputArea = null;
}
}
@@ -42,13 +42,22 @@ namespace osu.Game.Rulesets.Mania
var bindings = new List<KeyBinding>();
for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
bindings.Add(new KeyBinding(LeftKeys[i], currentAction++));
{
bindings.Add(new KeyBinding(LeftKeys[i], currentAction));
bindings.Add(new KeyBinding(InputKey.None, currentAction++));
}
if (columns % 2 == 1)
bindings.Add(new KeyBinding(SpecialKey, currentAction++));
{
bindings.Add(new KeyBinding(SpecialKey, currentAction));
bindings.Add(new KeyBinding(InputKey.None, currentAction++));
}
for (int i = 0; i < columns / 2; i++)
bindings.Add(new KeyBinding(RightKeys[i], currentAction++));
{
bindings.Add(new KeyBinding(RightKeys[i], currentAction));
bindings.Add(new KeyBinding(InputKey.None, currentAction++));
}
return bindings;
}
@@ -9,6 +9,7 @@ using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Visual;
@@ -284,5 +285,70 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
&& Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
});
}
[Test]
public void TestGridPlacementCommittedByDragSelection()
{
AddStep("add circle", () => EditorBeatmap.Add(new HitCircle
{
Position = new Vector2(64, 64),
StartTime = EditorClock.CurrentTime,
}));
AddStep("select circle tool", () => InputManager.Key(Key.Number2));
AddStep("select grid tool", () => InputManager.Key(Key.Number5));
AddStep("move cursor to centre", () => InputManager.MoveMouseTo(Editor));
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddStep("move cursor to (-1, -1)", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
InputManager.MoveMouseTo(composer.ToScreenSpace(new Vector2(-1, -1)));
});
AddStep("drag to center", () =>
{
InputManager.PressButton(MouseButton.Left);
InputManager.MoveMouseTo(Editor);
});
AddStep("release left", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("one selection", () => Editor.ChildrenOfType<OsuSelectionHandler>().Single().SelectedBlueprints, () => Has.One.Items);
AddAssert("selection is circle", () => Editor.ChildrenOfType<OsuSelectionHandler>().Single().SelectedBlueprints.Single(), Is.TypeOf<HitCircleSelectionBlueprint>);
AddStep("move cursor to slider", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.ElementAt(1)).EndPosition + new Vector2(1, 1)));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("one selection", () => Editor.ChildrenOfType<OsuSelectionHandler>().Single().SelectedBlueprints, () => Has.One.Items);
AddAssert("selection is slider", () => Editor.ChildrenOfType<OsuSelectionHandler>().Single().SelectedBlueprints.Single(), Is.TypeOf<SliderSelectionBlueprint>);
}
[Test]
public void TestGridPlacementRevertsToLastTool()
{
AddStep("select circle tool", () => InputManager.Key(Key.Number2));
AddStep("select grid tool", () => InputManager.Key(Key.Number5));
AddStep("move cursor to centre", () => InputManager.MoveMouseTo(Editor));
AddStep("start grid placement", () => InputManager.Click(MouseButton.Left));
AddStep("end grid placement", () => InputManager.Click(MouseButton.Left));
AddAssert("tool reverted to circle", () => getComposer().BlueprintContainer.CurrentTool, Is.TypeOf<HitCircleCompositionTool>);
HitObjectComposer getComposer() => Editor.ChildrenOfType<HitObjectComposer>().Single();
}
[Test]
public void TestGridPlacementDoesNotOverrideToolChange()
{
AddStep("select circle tool", () => InputManager.Key(Key.Number2));
AddStep("select grid tool", () => InputManager.Key(Key.Number5));
AddStep("move cursor to centre", () => InputManager.MoveMouseTo(Editor));
AddStep("start grid placement", () => InputManager.Click(MouseButton.Left));
AddStep("select circle tool again", () => InputManager.Key(Key.Number2));
AddAssert("circle tool selected", () => getComposer().BlueprintContainer.CurrentTool, Is.TypeOf<HitCircleCompositionTool>);
HitObjectComposer getComposer() => Editor.ChildrenOfType<HitObjectComposer>().Single();
}
}
}
@@ -32,26 +32,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
[Test]
public void TestPlayfieldBasedSize()
{
OsuModFlashlight flashlight;
CreateModTest(new ModTestData
{
Mods = [flashlight = new OsuModFlashlight(), new OsuModBarrelRoll()],
PassCondition = () =>
{
var flashlightOverlay = Player.DrawableRuleset.Overlays
.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>()
.First();
// the combo check is here because the flashlight radius decreases for the first time at 100 combo
// and hardcoding it here eliminates the need to meddle in flashlight internals further by e.g. exposing `GetComboScaleFor()`
return flashlightOverlay.GetSize() < flashlight.DefaultFlashlightSize && Player.GameplayState.ScoreProcessor.Combo.Value < 100;
}
});
}
[Test]
public void TestSliderDimsOnlyAfterStartTime()
{
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests
{
@@ -21,8 +22,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, []);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
}
@@ -32,8 +34,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, []);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
}
@@ -43,8 +46,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty();
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, [new OsuModHalfTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(2.22).Within(0.01));
@@ -55,8 +59,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty();
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, [new OsuModDoubleTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(7.77).Within(0.01));
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,162 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Scoring;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Utils;
using osu.Framework.Threading;
using osu.Game.Rulesets.Osu.HUD;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneAimErrorMeter : OsuManualInputManagerTestScene
{
private DependencyProvidingContainer dependencyContainer = null!;
private ScoreProcessor scoreProcessor = null!;
private TestAimErrorMeter aimErrorMeter = null!;
private CircularContainer gameObject = null!;
private ScheduledDelegate? automaticAdditionDelegate;
protected override void LoadComplete()
{
base.LoadComplete();
AddSliderStep("Hit marker size", 0f, 12f, 7f, t =>
{
if (aimErrorMeter.IsNotNull())
aimErrorMeter.HitMarkerSize.Value = t;
});
AddSliderStep("Average position marker size", 1f, 25f, 7f, t =>
{
if (aimErrorMeter.IsNotNull())
aimErrorMeter.AverageMarkerSize.Value = t;
});
}
[SetUpSteps]
public void SetupSteps() => AddStep("Create components", () =>
{
automaticAdditionDelegate?.Cancel();
automaticAdditionDelegate = null;
var ruleset = new OsuRuleset();
scoreProcessor = new ScoreProcessor(ruleset);
Child = dependencyContainer = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(ScoreProcessor), scoreProcessor)
}
};
dependencyContainer.Children = new Drawable[]
{
aimErrorMeter = new TestAimErrorMeter
{
Margin = new MarginPadding
{
Top = 100
},
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Scale = new Vector2(2),
},
gameObject = new CircularContainer
{
Size = new Vector2(2 * OsuHitObject.OBJECT_RADIUS),
Position = new Vector2(256, 192),
Colour = Color4.Yellow,
Masking = true,
BorderThickness = 2,
BorderColour = Color4.White,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
},
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(4),
}
}
}
};
});
protected override bool OnMouseDown(MouseDownEvent e)
{
// the division by 2 is because CS=5 applies a 0.5x (plus fudge) multiplier to `OBJECT_RADIUS`
aimErrorMeter.AddPoint((gameObject.ToLocalSpace(e.ScreenSpaceMouseDownPosition) - new Vector2(OsuHitObject.OBJECT_RADIUS)) / 2);
return true;
}
[Test]
public void TestManyHitPointsAutomatic()
{
AddStep("add scheduled delegate", () =>
{
automaticAdditionDelegate = Scheduler.AddDelayed(() =>
{
var randomPos = new Vector2(
RNG.NextSingle(0, 2 * OsuHitObject.OBJECT_RADIUS),
RNG.NextSingle(0, 2 * OsuHitObject.OBJECT_RADIUS));
aimErrorMeter.AddPoint(randomPos - new Vector2(OsuHitObject.OBJECT_RADIUS));
InputManager.MoveMouseTo(gameObject.ToScreenSpace(randomPos));
}, 1, true);
});
AddWaitStep("wait for some hit points", 10);
}
[Test]
public void TestDisplayStyles()
{
AddStep("Switch hit position marker style to +", () => aimErrorMeter.HitMarkerStyle.Value = AimErrorMeter.MarkerStyle.Plus);
AddStep("Switch hit position marker style to x", () => aimErrorMeter.HitMarkerStyle.Value = AimErrorMeter.MarkerStyle.X);
AddStep("Switch average position marker style to +", () => aimErrorMeter.AverageMarkerStyle.Value = AimErrorMeter.MarkerStyle.Plus);
AddStep("Switch average position marker style to x", () => aimErrorMeter.AverageMarkerStyle.Value = AimErrorMeter.MarkerStyle.X);
AddStep("Switch position display to absolute", () => aimErrorMeter.PositionDisplayStyle.Value = AimErrorMeter.PositionDisplay.Absolute);
AddStep("Switch position display to relative", () => aimErrorMeter.PositionDisplayStyle.Value = AimErrorMeter.PositionDisplay.Normalised);
}
[Test]
public void TestManualPlacement()
{
AddStep("return user input", () => InputManager.UseParentInput = true);
}
private partial class TestAimErrorMeter : AimErrorMeter
{
public void AddPoint(Vector2 position)
{
OnNewJudgement(new OsuHitCircleJudgementResult(new HitCircle(), new OsuJudgement())
{
CursorPositionAtHit = position
});
}
}
}
}
@@ -0,0 +1,160 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneDrawableJudgementSliderTicks : OsuSkinnableTestScene
{
private bool classic;
private readonly JudgementPooler<DrawableOsuJudgement>[] judgementPools;
public TestSceneDrawableJudgementSliderTicks()
{
judgementPools = new JudgementPooler<DrawableOsuJudgement>[Rows * Cols];
}
protected override void LoadComplete()
{
base.LoadComplete();
int cellIndex = 0;
SetContents(_ =>
{
var container = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
judgementPools[cellIndex] = new JudgementPooler<DrawableOsuJudgement>(new[]
{
HitResult.Great,
HitResult.Miss,
HitResult.LargeTickHit,
HitResult.SliderTailHit,
HitResult.LargeTickMiss,
HitResult.IgnoreMiss,
}),
new GridContainer
{
Padding = new MarginPadding { Top = 26f },
RelativeSizeAxes = Axes.Both,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
Content =
new[]
{
new[]
{
Empty(),
new OsuSpriteText
{
Text = "hit",
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
},
new OsuSpriteText
{
Text = "miss",
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
},
},
}.Concat(new[]
{
"head",
"tick",
"repeat",
"tail",
"slider",
}.Select(label => new Drawable[]
{
new OsuSpriteText
{
Text = label,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
},
new Container<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
new Container<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
})).ToArray(),
},
},
};
cellIndex++;
return container;
});
AddToggleStep("Toggle classic behaviour", c => classic = c);
AddStep("Show judgements", createAllJudgements);
}
private void createAllJudgements()
{
for (int cellIndex = 0; cellIndex < Rows * Cols; cellIndex++)
{
var slider = new Slider { StartTime = Time.Current, ClassicSliderBehaviour = classic };
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableHitObjects = new DrawableOsuHitObject[]
{
new DrawableSliderHead(new SliderHeadCircle { StartTime = Time.Current, ClassicSliderBehaviour = classic }),
new DrawableSliderTick(new SliderTick { StartTime = Time.Current }),
new DrawableSliderRepeat(new SliderRepeat(slider) { StartTime = Time.Current }),
new DrawableSliderTail(new SliderTailCircle(slider) { StartTime = Time.Current, ClassicSliderBehaviour = classic }),
new DrawableSlider(slider),
};
var containers = Cell(cellIndex).ChildrenOfType<Container<DrawableOsuJudgement>>().ToArray();
for (int i = 0; i < drawableHitObjects.Length; i++)
{
createJudgement(judgementPools[cellIndex], containers[i * 2], drawableHitObjects[i], true);
createJudgement(judgementPools[cellIndex], containers[i * 2 + 1], drawableHitObjects[i], false);
}
}
}
private void createJudgement(JudgementPooler<DrawableOsuJudgement> pool, Container<DrawableOsuJudgement> container, DrawableOsuHitObject drawableHitObject, bool hit)
{
container.Clear(false);
if (!drawableHitObject.DisplayResult)
return;
var hitObject = drawableHitObject.HitObject;
var result = new OsuJudgementResult(hitObject, hitObject.Judgement)
{
Type = hit ? hitObject.Judgement.MaxResult : hitObject.Judgement.MinResult,
};
var judgement = pool.Get(result.Type, d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
d.Scale = new Vector2(0.7f);
d.Apply(result, null);
});
if (judgement != null)
container.Add(judgement);
}
}
}
@@ -475,7 +475,7 @@ namespace osu.Game.Rulesets.Osu.Tests
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_stack_start - 450, Position = new Vector2(55), Actions = { OsuAction.LeftButton } },
});
}, extraMods: [new OsuModNoFail()]);
addClickActionAssert(0, ClickAction.Ignore);
}
@@ -21,6 +21,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Storyboards;
using osu.Game.Tests;
using osu.Game.Tests.Visual;
using osuTK;
@@ -152,6 +153,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}
[Test]
[FlakyTest]
public void TestSpinPerMinuteOnRewind()
{
double estimatedSpm = 0;
@@ -36,9 +36,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
base.EndPlacement(commit);
// You typically only place the grid once, so we switch back to the last tool after placement.
if (commit && hitObjectComposer is OsuHitObjectComposer osuHitObjectComposer)
osuHitObjectComposer.SetLastTool();
// You typically only place the grid once, so we switch back to the last tool after placement -
// but only if the tool hasn't changed from under us (which is possible, as external tool changes will commit any ongoing placements, including this one)
if (commit && hitObjectComposer?.BlueprintContainer.CurrentTool is GridFromPointsTool)
hitObjectComposer.SetLastTool();
}
protected override bool OnClick(ClickEvent e)
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
if (context.InterpretedDifficulty > DifficultyRating.Easy)
yield break;
var hitObjects = context.Beatmap.HitObjects;
var hitObjects = context.CurrentDifficulty.Playable.HitObjects;
for (int i = 0; i < hitObjects.Count - 1; ++i)
{
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
foreach (var hitobject in context.Beatmap.HitObjects)
foreach (var hitobject in context.CurrentDifficulty.Playable.HitObjects)
{
switch (hitobject)
{
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var diff = context.Beatmap.Difficulty;
var diff = context.CurrentDifficulty.Playable.Difficulty;
Issue? issue;
if (HasMoreThanOneDecimalPlace("Approach rate", diff.ApproachRate, out issue))
@@ -0,0 +1,21 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
namespace osu.Game.Rulesets.Osu.Edit.Checks
{
public class CheckOsuLowestDiffDrainTime : CheckLowestDiffDrainTime
{
protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds()
{
// See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21#general
yield return (DifficultyRating.Hard, new TimeSpan(0, 3, 30).TotalMilliseconds, "Hard");
yield return (DifficultyRating.Insane, new TimeSpan(0, 4, 15).TotalMilliseconds, "Insane");
yield return (DifficultyRating.Expert, new TimeSpan(0, 5, 0).TotalMilliseconds, "Expert");
}
}
}
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
yield break;
var prevObservedTimeDistances = new List<ObservedTimeDistance>();
var hitObjects = context.Beatmap.HitObjects;
var hitObjects = context.CurrentDifficulty.Playable.HitObjects;
for (int i = 0; i < hitObjects.Count - 1; ++i)
{
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
if (context.InterpretedDifficulty > DifficultyRating.Easy)
yield break;
foreach (var hitObject in context.Beatmap.HitObjects)
foreach (var hitObject in context.CurrentDifficulty.Playable.HitObjects)
{
if (hitObject is Slider slider && slider.SpanDuration < span_duration_threshold)
yield return new IssueTemplateTooShort(this).Create(slider);
@@ -19,14 +19,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
double od = context.Beatmap.Difficulty.OverallDifficulty;
double od = context.CurrentDifficulty.Playable.Difficulty.OverallDifficulty;
// These are meant to reflect the duration necessary for auto to score at least 1000 points on the spinner.
// It's difficult to eliminate warnings here, as auto achieving 1000 points depends on the approach angle on some spinners.
double warningThreshold = 500 + (od < 5 ? (5 - od) * -21.8 : (od - 5) * 20); // Anything above this is always ok.
double problemThreshold = 450 + (od < 5 ? (5 - od) * -17 : (od - 5) * 17); // Anything below this is never ok.
foreach (var hitObject in context.Beatmap.HitObjects)
foreach (var hitObject in context.CurrentDifficulty.Playable.HitObjects)
{
if (!(hitObject is Spinner spinner))
continue;
@@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Edit
new CheckTimeDistanceEquality(),
new CheckLowDiffOverlaps(),
new CheckTooShortSliders(),
new CheckOsuLowestDiffDrainTime(),
// Settings
new CheckOsuAbnormalDifficultySettings(),
@@ -176,6 +176,11 @@ namespace osu.Game.Rulesets.Osu.Edit
private void applyPosition()
{
// can happen if popover is dismissed by a keyboard key press while dragging UI controls
// it doesn't cause a crash, but it looks wrong
if (!editorBeatmap.TransactionActive)
return;
editorBeatmap.PerformOnSelection(ho =>
{
if (!initialPositions.TryGetValue(ho, out var initialPosition))
@@ -157,6 +157,10 @@ namespace osu.Game.Rulesets.Osu.Edit
rotationInfo.BindValueChanged(rotation =>
{
// can happen if the popover is dismissed by a keyboard key press while dragging UI controls
if (!rotationHandler.OperationInProgress.Value)
return;
rotationHandler.Update(rotation.NewValue.Degrees, getOriginPosition(rotation.NewValue));
});
}
@@ -220,6 +220,10 @@ namespace osu.Game.Rulesets.Osu.Edit
scaleInfo.BindValueChanged(scale =>
{
// can happen if the popover is dismissed by a keyboard key press while dragging UI controls
if (!scaleHandler.OperationInProgress.Value)
return;
var newScale = new Vector2(scale.NewValue.Scale, scale.NewValue.Scale);
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), getRotation(scale.NewValue));
});
+475
View File
@@ -0,0 +1,475 @@
// 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.ComponentModel;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Localisation.HUD;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osuTK;
using osuTK.Graphics;
using Container = osu.Framework.Graphics.Containers.Container;
namespace osu.Game.Rulesets.Osu.HUD
{
[Cached]
public partial class AimErrorMeter : HitErrorMeter
{
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.HitMarkerSize), nameof(AimErrorMeterStrings.HitMarkerSizeDescription))]
public BindableNumber<float> HitMarkerSize { get; } = new BindableNumber<float>(7f)
{
MinValue = 0f,
MaxValue = 12f,
Precision = 1f
};
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.HitMarkerStyle), nameof(AimErrorMeterStrings.HitMarkerStyleDescription))]
public Bindable<MarkerStyle> HitMarkerStyle { get; } = new Bindable<MarkerStyle>();
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.AverageMarkerSize), nameof(AimErrorMeterStrings.AverageMarkerSizeDescription))]
public BindableNumber<float> AverageMarkerSize { get; } = new BindableNumber<float>(12f)
{
MinValue = 7f,
MaxValue = 25f,
Precision = 1f
};
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.AverageMarkerStyle), nameof(AimErrorMeterStrings.AverageMarkerStyleDescription))]
public Bindable<MarkerStyle> AverageMarkerStyle { get; } = new Bindable<MarkerStyle>(MarkerStyle.Plus);
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.PositionDisplayStyle), nameof(AimErrorMeterStrings.PositionDisplayStyleDescription))]
public Bindable<PositionDisplay> PositionDisplayStyle { get; } = new Bindable<PositionDisplay>();
// used for calculate relative position.
private Vector2? lastObjectPosition;
private Container averagePositionMarker = null!;
private Container averagePositionMarkerRotationContainer = null!;
private Vector2? averagePosition;
private readonly DrawablePool<HitPositionMarker> hitPositionPool = new DrawablePool<HitPositionMarker>(30);
private Container hitPositionMarkerContainer = null!;
private Container arrowBackgroundContainer = null!;
private UprightAspectMaintainingContainer rotateFixedContainer = null!;
private Container mainContainer = null!;
private float objectRadius;
private const int max_concurrent_judgements = 30;
private const float line_thickness = 2;
private const float inner_portion = 0.85f;
[Resolved]
private OsuColour colours { get; set; } = null!;
public AimErrorMeter()
{
AutoSizeAxes = Axes.Both;
AlwaysPresent = true;
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, ScoreProcessor processor)
{
InternalChild = new Container
{
Height = 100,
Width = 100,
Children = new Drawable[]
{
hitPositionPool,
rotateFixedContainer = new UprightAspectMaintainingContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
}
};
mainContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BorderColour = Colour4.White,
Masking = true,
BorderThickness = 2,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(inner_portion),
Child = new Box
{
Colour = Colour4.Gray,
Alpha = 0.3f,
RelativeSizeAxes = Axes.Both
},
},
arrowBackgroundContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Name = "Arrow Background",
RelativeSizeAxes = Axes.Both,
Rotation = 45,
Alpha = 0f,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = inner_portion + 0.2f,
Width = line_thickness / 2,
},
new Circle
{
Height = 5f,
Width = line_thickness / 2,
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding(-line_thickness / 4),
RelativePositionAxes = Axes.Both,
Y = -(inner_portion + 0.2f) / 2,
Rotation = -45
},
new Circle
{
Height = 5f,
Width = line_thickness / 2,
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding(-line_thickness / 4),
RelativePositionAxes = Axes.Both,
Y = -(inner_portion + 0.2f) / 2,
Rotation = 45
}
}
},
new Container
{
Name = "Cross Background",
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.5f,
Width = line_thickness,
Height = inner_portion * 0.9f
},
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.5f,
Width = line_thickness,
Height = inner_portion * 0.9f,
Rotation = 90
},
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.2f,
Width = line_thickness / 2,
Height = inner_portion * 0.9f,
Rotation = 45
},
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.2f,
Width = line_thickness / 2,
Height = inner_portion * 0.9f,
Rotation = 135
},
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
hitPositionMarkerContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
averagePositionMarker = new UprightAspectMaintainingContainer
{
RelativePositionAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = averagePositionMarkerRotationContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.25f,
},
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.25f,
Rotation = 90
}
}
}
}
}
}
}
};
// handle IApplicableToDifficulty for CS change.
BeatmapDifficulty newDifficulty = new BeatmapDifficulty();
beatmap.Value.Beatmap.Difficulty.CopyTo(newDifficulty);
var mods = processor.Mods.Value;
foreach (var mod in mods.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(newDifficulty);
objectRadius = OsuHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(newDifficulty.CircleSize, true);
AverageMarkerSize.BindValueChanged(size => averagePositionMarker.Size = new Vector2(size.NewValue), true);
AverageMarkerStyle.BindValueChanged(style => averagePositionMarkerRotationContainer.Rotation = style.NewValue == MarkerStyle.Plus ? 0 : 45, true);
PositionDisplayStyle.BindValueChanged(s =>
{
Clear();
if (s.NewValue == PositionDisplay.Normalised)
{
arrowBackgroundContainer.FadeIn(100);
rotateFixedContainer.Remove(mainContainer, false);
AddInternal(mainContainer);
}
else
{
arrowBackgroundContainer.FadeOut(100);
// when in absolute mode, rotation of the aim error meter as a whole should not affect how the component is displayed
RemoveInternal(mainContainer, false);
rotateFixedContainer.Add(mainContainer);
}
}, true);
}
protected override void OnNewJudgement(JudgementResult judgement)
{
if (judgement is not OsuHitCircleJudgementResult circleJudgement) return;
if (circleJudgement.CursorPositionAtHit == null) return;
if (hitPositionMarkerContainer.Count > max_concurrent_judgements)
{
const double quick_fade_time = 300;
// check with a bit of lenience to avoid precision error in comparison.
var old = hitPositionMarkerContainer.FirstOrDefault(j => j.LifetimeEnd > Clock.CurrentTime + quick_fade_time * 1.1);
if (old != null)
{
old.ClearTransforms();
old.FadeOut(quick_fade_time).Expire();
}
}
Vector2 hitPosition;
if (PositionDisplayStyle.Value == PositionDisplay.Normalised && lastObjectPosition != null)
{
hitPosition = AccuracyHeatmap.FindRelativeHitPosition(lastObjectPosition.Value, ((OsuHitObject)circleJudgement.HitObject).StackedEndPosition,
circleJudgement.CursorPositionAtHit.Value, objectRadius, 45) * (inner_portion / 2);
}
else
{
// get relative position between mouse position and current object.
hitPosition = (circleJudgement.CursorPositionAtHit.Value - ((OsuHitObject)circleJudgement.HitObject).StackedPosition) / objectRadius / 2 * inner_portion;
}
hitPosition = Vector2.Clamp(hitPosition, new Vector2(-0.5f), new Vector2(0.5f));
hitPositionPool.Get(drawableHit =>
{
drawableHit.X = hitPosition.X;
drawableHit.Y = hitPosition.Y;
drawableHit.Colour = getColourForPosition(hitPosition);
hitPositionMarkerContainer.Add(drawableHit);
});
var newAveragePosition = 0.1f * hitPosition + 0.9f * (averagePosition ?? hitPosition);
averagePositionMarker.MoveTo(newAveragePosition, 800, Easing.OutQuint);
averagePosition = newAveragePosition;
lastObjectPosition = ((OsuHitObject)circleJudgement.HitObject).StackedPosition;
}
private Color4 getColourForPosition(Vector2 position)
{
float distance = Vector2.Distance(position, Vector2.Zero);
if (distance >= 0.5f * inner_portion)
return colours.Red;
if (distance >= 0.35f * inner_portion)
return colours.Yellow;
if (distance >= 0.2f * inner_portion)
return colours.Green;
return colours.Blue;
}
public override void Clear()
{
averagePosition = null;
averagePositionMarker.MoveTo(Vector2.Zero, 800, Easing.OutQuint);
lastObjectPosition = null;
foreach (var h in hitPositionMarkerContainer)
{
h.ClearTransforms();
h.Expire();
}
}
private partial class HitPositionMarker : PoolableDrawable
{
[Resolved]
private AimErrorMeter aimErrorMeter { get; set; } = null!;
public readonly BindableNumber<float> MarkerSize = new BindableFloat();
public readonly Bindable<MarkerStyle> Style = new Bindable<MarkerStyle>();
private readonly Container content;
public HitPositionMarker()
{
RelativePositionAxes = Axes.Both;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChild = new UprightAspectMaintainingContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = content = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.25f,
Rotation = -45
},
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.25f,
Rotation = 45
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
MarkerSize.BindTo(aimErrorMeter.HitMarkerSize);
MarkerSize.BindValueChanged(size => Size = new Vector2(size.NewValue), true);
Style.BindTo(aimErrorMeter.HitMarkerStyle);
Style.BindValueChanged(style => content.Rotation = style.NewValue == MarkerStyle.X ? 0 : 45, true);
}
protected override void PrepareForUse()
{
base.PrepareForUse();
const int judgement_fade_in_duration = 100;
const int judgement_fade_out_duration = 5000;
this
.ResizeTo(new Vector2(0))
.FadeInFromZero(judgement_fade_in_duration, Easing.OutQuint)
.ResizeTo(new Vector2(MarkerSize.Value), judgement_fade_in_duration, Easing.OutQuint)
.Then()
.FadeOut(judgement_fade_out_duration)
.Expire();
}
}
public enum MarkerStyle
{
[Description("x")]
X,
[Description("+")]
Plus,
}
public enum PositionDisplay
{
[LocalisableDescription(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.Absolute))]
Absolute,
[LocalisableDescription(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.Normalised))]
Normalised,
}
}
}
@@ -5,6 +5,7 @@ using System;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Name => @"Alternate";
public override string Acronym => @"AL";
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 => OsuIcon.ModAlternate;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action;
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
@@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "AD";
public override LocalisableString Description => "Never trust the approach circles...";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
public override IconUsage? Icon => OsuIcon.ModApproachDifferent;
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModFreezeFrame) };
+2 -1
View File
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
@@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => "Play with blinds on your screen.";
public override string Acronym => "BL";
public override IconUsage? Icon => FontAwesome.Solid.Adjust;
public override IconUsage? Icon => OsuIcon.ModBlinds;
public override ModType Type => ModType.DifficultyIncrease;
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;

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