Compare commits
2167 Commits
@@ -21,7 +21,7 @@
|
||||
]
|
||||
},
|
||||
"ppy.localisationanalyser.tools": {
|
||||
"version": "2024.802.0",
|
||||
"version": "2025.1208.0",
|
||||
"commands": [
|
||||
"localisation"
|
||||
]
|
||||
|
||||
@@ -19,6 +19,11 @@ indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130051/Cannot-resolve-symbol-inspections-incorrectly-firing-for-xmldoc-protected-member-references
|
||||
resharper_c_sharp_warnings_cs1574_cs1584_cs1581_cs1580_highlighting = hint
|
||||
# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130381/Rider-does-not-respect-propagated-NoWarn-CS1591?backToIssues=false
|
||||
dotnet_diagnostic.CS1591.severity = none
|
||||
|
||||
#license header
|
||||
file_header_template = Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text.
|
||||
|
||||
|
||||
@@ -145,5 +145,10 @@ jobs:
|
||||
- name: Install .NET Workloads
|
||||
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
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Copy labels from linked issues
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
copy-labels:
|
||||
runs-on: ubuntu-latest
|
||||
name: Copy labels from linked issues
|
||||
steps:
|
||||
- name: Copy labels
|
||||
uses: michalvankodev/copy-issue-labels@v1.3.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -55,9 +55,7 @@ When in doubt, it's probably best to start with a discussion first. We will esca
|
||||
|
||||
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
|
||||
|
||||
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good first issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
|
||||
|
||||
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
|
||||
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
|
||||
|
||||
If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library).
|
||||
|
||||
@@ -73,6 +71,9 @@ Aside from the above, below is a brief checklist of things to watch out when you
|
||||
After you're done with your changes and you wish to open the PR, please observe the following recommendations:
|
||||
|
||||
- Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary.
|
||||
- Please pick the following target branch for your pull request:
|
||||
- `pp-dev`, if the change impacts star rating or performance points calculations for any of the rulesets,
|
||||
- `master`, otherwise.
|
||||
- Please avoid pushing untested or incomplete code.
|
||||
- Please do not force-push or rebase unless we ask you to.
|
||||
- Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.715.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.303.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -189,7 +189,7 @@ namespace osu.Desktop
|
||||
}
|
||||
|
||||
// user party
|
||||
if (!hideIdentifiableInformation && multiplayerClient.Room != null)
|
||||
if (!hideIdentifiableInformation && multiplayerClient.Room != null && multiplayerClient.Room.Settings.MatchType != MatchType.Matchmaking)
|
||||
{
|
||||
MultiplayerRoom room = multiplayerClient.Room;
|
||||
|
||||
|
||||
@@ -146,9 +146,13 @@ namespace osu.Desktop
|
||||
{
|
||||
base.SetHost(host);
|
||||
|
||||
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||
if (iconStream != null)
|
||||
host.Window.SetIconFromStream(iconStream);
|
||||
// Apple operating systems use a better icon provided via external assets.
|
||||
if (!RuntimeInfo.IsApple)
|
||||
{
|
||||
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||
if (iconStream != null)
|
||||
host.Window.SetIconFromStream(iconStream);
|
||||
}
|
||||
|
||||
host.Window.Title = Name;
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
|
||||
@@ -32,7 +33,7 @@ namespace osu.Desktop.Security
|
||||
{
|
||||
public ElevatedPrivilegesNotification()
|
||||
{
|
||||
Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";
|
||||
Text = NotificationsStrings.ElevatedPrivileges(RuntimeInfo.IsUnix ? "root" : "Administrator");
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
@@ -0,0 +1,77 @@
|
||||
// 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.IO;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Benchmarks
|
||||
{
|
||||
public class BenchmarkDifficultyCalculation : BenchmarkTest
|
||||
{
|
||||
private DifficultyCalculator osuCalculator = null!;
|
||||
private DifficultyCalculator taikoCalculator = null!;
|
||||
private DifficultyCalculator catchCalculator = null!;
|
||||
private DifficultyCalculator maniaCalculator = null!;
|
||||
|
||||
public override void SetUp()
|
||||
{
|
||||
using var resources = new DllResourceStore(typeof(TestResources).Assembly);
|
||||
|
||||
using var archive = resources.GetStream("Resources/Archives/241526 Soleily - Renatus.osz");
|
||||
using var archiveReader = new ZipArchiveReader(archive);
|
||||
|
||||
var osuBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (Gamu) [Insane].osu");
|
||||
var taikoBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (MMzz) [Oni].osu");
|
||||
var catchBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (Deif) [Salad].osu");
|
||||
var maniaBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (ExPew) [Another].osu");
|
||||
|
||||
osuCalculator = new OsuRuleset().CreateDifficultyCalculator(osuBeatmap);
|
||||
taikoCalculator = new TaikoRuleset().CreateDifficultyCalculator(taikoBeatmap);
|
||||
catchCalculator = new CatchRuleset().CreateDifficultyCalculator(catchBeatmap);
|
||||
maniaCalculator = new ManiaRuleset().CreateDifficultyCalculator(maniaBeatmap);
|
||||
}
|
||||
|
||||
private WorkingBeatmap readBeatmap(ZipArchiveReader archiveReader, string beatmapName)
|
||||
{
|
||||
using var beatmapStream = new MemoryStream();
|
||||
archiveReader.GetStream(beatmapName).CopyTo(beatmapStream);
|
||||
|
||||
beatmapStream.Seek(0, SeekOrigin.Begin);
|
||||
using var reader = new LineBufferedReader(beatmapStream);
|
||||
|
||||
var decoder = Beatmaps.Formats.Decoder.GetDecoder<Beatmap>(reader);
|
||||
return new FlatWorkingBeatmap(decoder.Decode(reader));
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyOsu() => osuCalculator.Calculate();
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyTaiko() => taikoCalculator.Calculate();
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyCatch() => catchCalculator.Calculate();
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyMania() => maniaCalculator.Calculate();
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyOsuHundredTimes()
|
||||
{
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
osuCalculator.Calculate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,11 +35,9 @@
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
|
||||
@@ -22,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.GetAdjustedDisplayDifficulty(difficulty, []);
|
||||
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, []);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
|
||||
}
|
||||
@@ -33,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.GetAdjustedDisplayDifficulty(difficulty, [new CatchModHalfTime()]);
|
||||
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, [new CatchModHalfTime()]);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
|
||||
}
|
||||
@@ -44,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.GetAdjustedDisplayDifficulty(difficulty, [new CatchModDoubleTime()]);
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,2 +0,0 @@
|
||||
[General]
|
||||
// no version specified means v1
|
||||
@@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTinyDropletMissPreservesCatcherState()
|
||||
public void TestTinyDropletMissChangesCatcherState()
|
||||
{
|
||||
AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit
|
||||
{
|
||||
@@ -165,8 +165,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}));
|
||||
AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
|
||||
AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 }));
|
||||
// catcher state and hyper dash state is preserved
|
||||
checkState(CatcherAnimationState.Kiai);
|
||||
// catcher state is changed but hyper dash state is preserved
|
||||
checkState(CatcherAnimationState.Fail);
|
||||
checkHyperDash(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -151,6 +154,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
new CatchModFloatingFruits(),
|
||||
new CatchModMuted(),
|
||||
new CatchModNoScope(),
|
||||
new CatchModMovingFast(),
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
@@ -172,15 +176,20 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
|
||||
|
||||
protected override IEnumerable<HitResult> GetValidHitResults()
|
||||
public override IEnumerable<HitResult> GetValidHitResults()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
HitResult.Great,
|
||||
HitResult.Miss,
|
||||
|
||||
HitResult.LargeTickHit,
|
||||
HitResult.LargeTickMiss,
|
||||
HitResult.SmallTickHit,
|
||||
HitResult.SmallTickMiss,
|
||||
HitResult.LargeBonus,
|
||||
HitResult.IgnoreHit,
|
||||
HitResult.IgnoreMiss,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -266,9 +275,9 @@ namespace osu.Game.Rulesets.Catch
|
||||
}
|
||||
|
||||
/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
|
||||
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
|
||||
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);
|
||||
@@ -278,6 +287,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.DifficultyRangeInt(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
private float halfCatcherWidth;
|
||||
|
||||
public override int Version => 20250306;
|
||||
public override int Version => 20251020;
|
||||
|
||||
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
@@ -51,15 +51,13 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
// Combo scaling
|
||||
if (catchAttributes.MaxCombo > 0)
|
||||
value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
|
||||
value *= Math.Min(Math.Pow(score.MaxCombo, 0.35) / Math.Pow(catchAttributes.MaxCombo, 0.35), 1.0);
|
||||
|
||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||
|
||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
double clockRate = track.Rate;
|
||||
double clockRate = ModUtils.CalculateRateWithMods(score.Mods);
|
||||
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty.Evaluators
|
||||
{
|
||||
public static class MovementEvaluator
|
||||
{
|
||||
private const double direction_change_bonus = 21.0;
|
||||
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, double catcherSpeedMultiplier)
|
||||
{
|
||||
var catchCurrent = (CatchDifficultyHitObject)current;
|
||||
var catchLast = (CatchDifficultyHitObject)current.Previous(0);
|
||||
var catchLastLast = (CatchDifficultyHitObject)current.Previous(1);
|
||||
|
||||
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
||||
|
||||
double distanceAddition = (Math.Pow(Math.Abs(catchCurrent.DistanceMoved), 1.3) / 510);
|
||||
double sqrtStrain = Math.Sqrt(weightedStrainTime);
|
||||
|
||||
double edgeDashBonus = 0;
|
||||
|
||||
// Direction change bonus.
|
||||
if (Math.Abs(catchCurrent.DistanceMoved) > 0.1)
|
||||
{
|
||||
if (current.Index >= 1 && Math.Abs(catchLast.DistanceMoved) > 0.1 && Math.Sign(catchCurrent.DistanceMoved) != Math.Sign(catchLast.DistanceMoved))
|
||||
{
|
||||
double bonusFactor = Math.Min(50, Math.Abs(catchCurrent.DistanceMoved)) / 50;
|
||||
double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(catchLast.DistanceMoved)) / 70, 0.38);
|
||||
|
||||
distanceAddition += direction_change_bonus / Math.Sqrt(catchLast.StrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
|
||||
}
|
||||
|
||||
// Base bonus for every movement, giving some weight to streams.
|
||||
distanceAddition += 12.5 * Math.Min(Math.Abs(catchCurrent.DistanceMoved), CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 2)
|
||||
/ (CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 6) / sqrtStrain;
|
||||
}
|
||||
|
||||
// Bonus for edge dashes.
|
||||
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
|
||||
{
|
||||
if (!catchCurrent.LastObject.HyperDash)
|
||||
edgeDashBonus += 5.7;
|
||||
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20)
|
||||
* Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
}
|
||||
|
||||
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
|
||||
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and NORMALIZED_HALF_CATCHER_WIDTH offsets
|
||||
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
|
||||
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and NORMALIZED_HALF_CATCHER_WIDTH)
|
||||
if (current.Index >= 2 && Math.Abs(catchCurrent.ExactDistanceMoved) <= CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 2
|
||||
&& catchCurrent.ExactDistanceMoved == -catchLast.ExactDistanceMoved && catchLast.ExactDistanceMoved == -catchLastLast.ExactDistanceMoved
|
||||
&& catchCurrent.StrainTime == catchLast.StrainTime && catchLast.StrainTime == catchLastLast.StrainTime)
|
||||
distanceAddition = 0;
|
||||
|
||||
return distanceAddition / weightedStrainTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,49 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
||||
{
|
||||
public class CatchDifficultyHitObject : DifficultyHitObject
|
||||
{
|
||||
private const float normalized_hitobject_radius = 41.0f;
|
||||
public const float NORMALIZED_HALF_CATCHER_WIDTH = 41.0f;
|
||||
private const float absolute_player_positioning_error = 16.0f;
|
||||
|
||||
public new PalpableCatchHitObject BaseObject => (PalpableCatchHitObject)base.BaseObject;
|
||||
|
||||
public new PalpableCatchHitObject LastObject => (PalpableCatchHitObject)base.LastObject;
|
||||
|
||||
/// <summary>
|
||||
/// Normalized position of <see cref="BaseObject"/>.
|
||||
/// </summary>
|
||||
public readonly float NormalizedPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Normalized position of <see cref="LastObject"/>.
|
||||
/// </summary>
|
||||
public readonly float LastNormalizedPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Normalized position of the player required to catch <see cref="BaseObject"/>, assuming the player moves as little as possible.
|
||||
/// </summary>
|
||||
public float PlayerPosition { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized position of the player after catching <see cref="LastObject"/>.
|
||||
/// </summary>
|
||||
public float LastPlayerPosition { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized distance between <see cref="LastPlayerPosition"/> and <see cref="PlayerPosition"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The sign of the value indicates the direction of the movement: negative is left and positive is right.
|
||||
/// </remarks>
|
||||
public float DistanceMoved { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized distance the player has to move from <see cref="LastPlayerPosition"/> in order to catch <see cref="BaseObject"/> at its <see cref="NormalizedPosition"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The sign of the value indicates the direction of the movement: negative is left and positive is right.
|
||||
/// </remarks>
|
||||
public float ExactDistanceMoved { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds elapsed since the start time of the previous <see cref="CatchDifficultyHitObject"/>, with a minimum of 40ms.
|
||||
/// </summary>
|
||||
@@ -29,13 +63,35 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
||||
: base(hitObject, lastObject, clockRate, objects, index)
|
||||
{
|
||||
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||
float scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
|
||||
float scalingFactor = NORMALIZED_HALF_CATCHER_WIDTH / halfCatcherWidth;
|
||||
|
||||
NormalizedPosition = BaseObject.EffectiveX * scalingFactor;
|
||||
LastNormalizedPosition = LastObject.EffectiveX * scalingFactor;
|
||||
|
||||
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
|
||||
StrainTime = Math.Max(40, DeltaTime);
|
||||
|
||||
setMovementState();
|
||||
}
|
||||
|
||||
private void setMovementState()
|
||||
{
|
||||
LastPlayerPosition = Index == 0 ? LastNormalizedPosition : ((CatchDifficultyHitObject)Previous(0)).PlayerPosition;
|
||||
|
||||
PlayerPosition = Math.Clamp(
|
||||
LastPlayerPosition,
|
||||
NormalizedPosition - (NORMALIZED_HALF_CATCHER_WIDTH - absolute_player_positioning_error),
|
||||
NormalizedPosition + (NORMALIZED_HALF_CATCHER_WIDTH - absolute_player_positioning_error)
|
||||
);
|
||||
|
||||
DistanceMoved = PlayerPosition - LastPlayerPosition;
|
||||
|
||||
// For the exact position we consider that the catcher is in the correct position for both objects
|
||||
ExactDistanceMoved = NormalizedPosition - LastPlayerPosition;
|
||||
|
||||
// After a hyperdash we ARE in the correct position. Always!
|
||||
if (LastObject.HyperDash)
|
||||
PlayerPosition = NormalizedPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +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 osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Catch.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -11,10 +10,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
{
|
||||
public class Movement : StrainDecaySkill
|
||||
{
|
||||
private const float absolute_player_positioning_error = 16f;
|
||||
private const float normalized_hitobject_radius = 41.0f;
|
||||
private const double direction_change_bonus = 21.0;
|
||||
|
||||
protected override double SkillMultiplier => 1;
|
||||
protected override double StrainDecayBase => 0.2;
|
||||
|
||||
@@ -24,12 +19,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
protected readonly float HalfCatcherWidth;
|
||||
|
||||
private float? lastPlayerPosition;
|
||||
private float lastDistanceMoved;
|
||||
private float lastExactDistanceMoved;
|
||||
private double lastStrainTime;
|
||||
private bool isInBuzzSection;
|
||||
|
||||
/// <summary>
|
||||
/// The speed multiplier applied to the player's catcher.
|
||||
/// </summary>
|
||||
@@ -49,80 +38,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
var catchCurrent = (CatchDifficultyHitObject)current;
|
||||
|
||||
lastPlayerPosition ??= catchCurrent.LastNormalizedPosition;
|
||||
|
||||
float playerPosition = Math.Clamp(
|
||||
lastPlayerPosition.Value,
|
||||
catchCurrent.NormalizedPosition - (normalized_hitobject_radius - absolute_player_positioning_error),
|
||||
catchCurrent.NormalizedPosition + (normalized_hitobject_radius - absolute_player_positioning_error)
|
||||
);
|
||||
|
||||
float distanceMoved = playerPosition - lastPlayerPosition.Value;
|
||||
|
||||
// For the exact position we consider that the catcher is in the correct position for both objects
|
||||
float exactDistanceMoved = catchCurrent.NormalizedPosition - lastPlayerPosition.Value;
|
||||
|
||||
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
||||
|
||||
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
|
||||
double sqrtStrain = Math.Sqrt(weightedStrainTime);
|
||||
|
||||
double edgeDashBonus = 0;
|
||||
|
||||
// Direction change bonus.
|
||||
if (Math.Abs(distanceMoved) > 0.1)
|
||||
{
|
||||
if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved))
|
||||
{
|
||||
double bonusFactor = Math.Min(50, Math.Abs(distanceMoved)) / 50;
|
||||
double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(lastDistanceMoved)) / 70, 0.38);
|
||||
|
||||
distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
|
||||
}
|
||||
|
||||
// Base bonus for every movement, giving some weight to streams.
|
||||
distanceAddition += 12.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
|
||||
}
|
||||
|
||||
// Bonus for edge dashes.
|
||||
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
|
||||
{
|
||||
if (!catchCurrent.LastObject.HyperDash)
|
||||
edgeDashBonus += 5.7;
|
||||
else
|
||||
{
|
||||
// After a hyperdash we ARE in the correct position. Always!
|
||||
playerPosition = catchCurrent.NormalizedPosition;
|
||||
}
|
||||
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20)
|
||||
* Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
}
|
||||
|
||||
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
|
||||
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and normalized_hitobject_radius offsets
|
||||
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
|
||||
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius)
|
||||
if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime)
|
||||
{
|
||||
if (isInBuzzSection)
|
||||
distanceAddition = 0;
|
||||
else
|
||||
isInBuzzSection = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isInBuzzSection = false;
|
||||
}
|
||||
|
||||
lastPlayerPosition = playerPosition;
|
||||
lastDistanceMoved = distanceMoved;
|
||||
lastStrainTime = catchCurrent.StrainTime;
|
||||
lastExactDistanceMoved = exactDistanceMoved;
|
||||
|
||||
return distanceAddition / weightedStrainTime;
|
||||
return MovementEvaluator.EvaluateDifficultyOf(current, catcherSpeedMultiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
private double placementStartTime;
|
||||
private double placementEndTime;
|
||||
|
||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
|
||||
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
|
||||
|
||||
public BananaShowerPlacementBlueprint()
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
|
||||
private InputManager inputManager = null!;
|
||||
|
||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
|
||||
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
|
||||
|
||||
public JuiceStreamPlacementBlueprint()
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -224,7 +225,8 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
#region Clipboard handling
|
||||
|
||||
public override string ConvertSelectionToString()
|
||||
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<CatchHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
|
||||
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<CatchHitObject>().OrderBy(h => h.StartTime)
|
||||
.Select(h => (h.IndexInCurrentCombo + 1).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
// 1,2,3,4 ...
|
||||
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -51,7 +51,8 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
string format(string acronym, DifficultyBindable bindable)
|
||||
=> $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@@ -53,13 +54,25 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
public override IEnumerable<string> LookupNames => lookup_names;
|
||||
|
||||
public BananaHitSampleInfo(int volume = 100)
|
||||
: base(string.Empty, volume: volume)
|
||||
public BananaHitSampleInfo()
|
||||
: this(string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
|
||||
=> new BananaHitSampleInfo(newVolume.GetOr(Volume));
|
||||
public BananaHitSampleInfo(HitSampleInfo info)
|
||||
: this(info.Name, info.Bank, info.Suffix, info.Volume, info.EditorAutoBank, info.UseBeatmapSamples)
|
||||
{
|
||||
}
|
||||
|
||||
private BananaHitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100, bool editorAutoBank = true, bool useBeatmapSamples = false)
|
||||
: base(name, bank, suffix, volume, editorAutoBank, useBeatmapSamples)
|
||||
{
|
||||
}
|
||||
|
||||
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default,
|
||||
Optional<bool> newEditorAutoBank = default, Optional<bool> newUseBeatmapSamples = default)
|
||||
=> new BananaHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume),
|
||||
newEditorAutoBank.GetOr(EditorAutoBank), newUseBeatmapSamples.GetOr(UseBeatmapSamples));
|
||||
|
||||
public bool Equals(BananaHitSampleInfo? other)
|
||||
=> other != null;
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
StartTime = time,
|
||||
BananaIndex = count,
|
||||
Samples = new List<HitSampleInfo> { new Banana.BananaHitSampleInfo(CreateHitSampleInfo().Volume) }
|
||||
Samples = new List<HitSampleInfo> { new Banana.BananaHitSampleInfo(CreateHitSampleInfo()) }
|
||||
});
|
||||
|
||||
count++;
|
||||
|
||||
@@ -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 = IBeatmapDifficultyInfo.DifficultyRangeInt(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.
|
||||
|
||||
@@ -72,6 +72,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
leaderboard.Origin = Anchor.CentreLeft;
|
||||
leaderboard.X = 10;
|
||||
}
|
||||
|
||||
foreach (var d in container.OfType<ISerialisableDrawable>())
|
||||
d.UsesFixedAnchor = true;
|
||||
})
|
||||
{
|
||||
Children = new Drawable[]
|
||||
|
||||
@@ -224,7 +224,20 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
addLighting(result, drawableObject.AccentColour.Value, positionInStack.X);
|
||||
}
|
||||
|
||||
// droplet doesn't affect the catcher state
|
||||
if (result.IsHit)
|
||||
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
|
||||
else if (hitObject is not Banana)
|
||||
CurrentState = CatcherAnimationState.Fail;
|
||||
|
||||
if (palpableObject.HitObject.LastInCombo)
|
||||
{
|
||||
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
|
||||
Explode();
|
||||
else
|
||||
Drop();
|
||||
}
|
||||
|
||||
// droplet doesn't affect hyperdash state
|
||||
if (hitObject is TinyDroplet) return;
|
||||
|
||||
// if a hyper fruit was already handled this frame, just go where it says to go.
|
||||
@@ -244,19 +257,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
else
|
||||
SetHyperDashState();
|
||||
}
|
||||
|
||||
if (result.IsHit)
|
||||
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
|
||||
else if (!(hitObject is Banana))
|
||||
CurrentState = CatcherAnimationState.Fail;
|
||||
|
||||
if (palpableObject.HitObject.LastInCombo)
|
||||
{
|
||||
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
|
||||
Explode();
|
||||
else
|
||||
Drop();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnRevertResult(JudgementResult result)
|
||||
|
||||
@@ -35,11 +35,9 @@
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@@ -16,6 +17,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
@@ -36,21 +38,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
[Test]
|
||||
public void TestPlaceBeforeCurrentTimeDownwards()
|
||||
{
|
||||
AddStep("seek to 200", () => HitObjectContainer.Dependencies.Get<EditorClock>().Seek(200));
|
||||
AddStep("move mouse before current time", () =>
|
||||
{
|
||||
var column = this.ChildrenOfType<Column>().Single();
|
||||
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(-100));
|
||||
});
|
||||
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("note start time < 0", () => getNote().StartTime < 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceAfterCurrentTimeDownwards()
|
||||
{
|
||||
AddStep("move mouse after current time", () =>
|
||||
{
|
||||
var column = this.ChildrenOfType<Column>().Single();
|
||||
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(100));
|
||||
@@ -58,7 +47,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("note start time > 0", () => getNote().StartTime > 0);
|
||||
AddAssert("note start time < 200", () => getNote().StartTime < 200);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceAfterCurrentTimeDownwards()
|
||||
{
|
||||
AddStep("seek to 200", () => HitObjectContainer.Dependencies.Get<EditorClock>().Seek(200));
|
||||
AddStep("move mouse after current time", () =>
|
||||
{
|
||||
var column = this.ChildrenOfType<Column>().Single();
|
||||
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(300));
|
||||
});
|
||||
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("note start time > 200", () => getNote().StartTime > 200);
|
||||
}
|
||||
|
||||
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
|
||||
|
||||
@@ -18,15 +18,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
public void TestNormalSelection()
|
||||
{
|
||||
addStepClickLink("00:05:920 (5920|3,6623|3,6857|2,7326|1)");
|
||||
AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, new List<(int, int)>
|
||||
{ (5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1) }
|
||||
));
|
||||
AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, [(5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1)]));
|
||||
|
||||
addReset();
|
||||
addStepClickLink("00:42:716 (42716|3,43420|2,44123|0,44357|1,45295|1)");
|
||||
AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, new List<(int, int)>
|
||||
{ (42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1) }
|
||||
));
|
||||
AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, [(42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1)]));
|
||||
|
||||
addReset();
|
||||
AddStep("add notes to row", () =>
|
||||
@@ -41,15 +37,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
EditorBeatmap.AddRange(new[] { second, third, forth });
|
||||
});
|
||||
addStepClickLink("00:11:545 (11545|0,11545|1,11545|2,11545|3)");
|
||||
AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, new List<(int, int)>
|
||||
{ (11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3) }
|
||||
));
|
||||
AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, [(11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3)]));
|
||||
|
||||
addReset();
|
||||
addStepClickLink("01:36:623 (96623|1,97560|1,97677|1,97795|1,98966|1)");
|
||||
AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, new List<(int, int)>
|
||||
{ (96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1) }
|
||||
));
|
||||
AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, [(96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1)]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRoundingToNearestMillisecondApplied()
|
||||
{
|
||||
AddStep("resnap note to have fractional coordinates",
|
||||
() => EditorBeatmap.HitObjects.OfType<ManiaHitObject>().Single(ho => ho.StartTime == 85_373 && ho.Column == 1).StartTime = 85_373.125);
|
||||
addStepClickLink("01:25:373 (85373|1)");
|
||||
AddAssert("selected note", () => checkSnapAndSelectColumn(85_373.125, [(85_373.125, 1)]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -75,7 +76,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
|
||||
private void addReset() => addStepClickLink("00:00:000", "reset", false);
|
||||
|
||||
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null)
|
||||
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(double, int)>? columnPairs = null)
|
||||
{
|
||||
bool checkColumns = columnPairs != null
|
||||
? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2)))
|
||||
|
||||
@@ -22,7 +22,9 @@ 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")]
|
||||
[TestCase("spinner-convert-samples")]
|
||||
public void Test(string name) => base.Test(name);
|
||||
|
||||
protected override IEnumerable<SampleConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
@@ -32,6 +34,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 +60,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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
Mode: 0
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,2,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,2,0:0:0:0:
|
||||
@@ -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
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"Mappings": [{
|
||||
"StartTime": 1000.0,
|
||||
"Objects": [{
|
||||
"StartTime": 1000.0,
|
||||
"EndTime": 8000.0,
|
||||
"Column": 0,
|
||||
"PlaySlidingSamples": false,
|
||||
"NodeSamples": [
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"]
|
||||
],
|
||||
"Samples": ["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"],
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
Mode: 0
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:5
|
||||
CircleSize:5
|
||||
OverallDifficulty:5
|
||||
ApproachRate:5
|
||||
SliderMultiplier:1.4
|
||||
SliderTickRate:1
|
||||
|
||||
[TimingPoints]
|
||||
0,500,4,2,0,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
256,192,1000,8,4,8000,0:2:0:0:
|
||||
@@ -45,5 +45,19 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AssertBeatmapLookup(expected_sample);
|
||||
AssertNoLookup(unwanted_sample);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConvertHitObjectCustomSampleBank()
|
||||
{
|
||||
const string beatmap_sample = "normal-hitwhistle2";
|
||||
const string user_skin_sample = "normal-hitnormal";
|
||||
|
||||
SetupSkins(beatmap_sample, user_skin_sample);
|
||||
|
||||
CreateTestWithBeatmap("convert-beatmap-custom-sample-bank.osu");
|
||||
|
||||
AssertBeatmapLookup(beatmap_sample);
|
||||
AssertUserLookup(user_skin_sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,7 +85,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
Duration = endTime - HitObject.StartTime,
|
||||
Column = column,
|
||||
Samples = HitObject.Samples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
|
||||
NodeSamples =
|
||||
[
|
||||
HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).ToList(),
|
||||
HitObject.Samples
|
||||
]
|
||||
};
|
||||
}
|
||||
else
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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.Utils;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty.Evaluators
|
||||
{
|
||||
public class IndividualStrainEvaluator
|
||||
{
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||
double startTime = maniaCurrent.StartTime;
|
||||
double endTime = maniaCurrent.EndTime;
|
||||
|
||||
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
||||
|
||||
// We award a bonus if this note starts and ends before the end of another hold note.
|
||||
foreach (var maniaPrevious in maniaCurrent.PreviousHitObjects)
|
||||
{
|
||||
if (maniaPrevious is null)
|
||||
continue;
|
||||
|
||||
if (Precision.DefinitelyBigger(maniaPrevious.EndTime, endTime, 1) &&
|
||||
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1))
|
||||
{
|
||||
holdFactor = 1.25;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 2.0 * holdFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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.Utils;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty.Evaluators
|
||||
{
|
||||
public class OverallStrainEvaluator
|
||||
{
|
||||
private const double release_threshold = 30;
|
||||
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||
double startTime = maniaCurrent.StartTime;
|
||||
double endTime = maniaCurrent.EndTime;
|
||||
bool isOverlapping = false;
|
||||
|
||||
double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information
|
||||
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
||||
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
|
||||
|
||||
foreach (var maniaPrevious in maniaCurrent.PreviousHitObjects)
|
||||
{
|
||||
if (maniaPrevious is null)
|
||||
continue;
|
||||
|
||||
// The current note is overlapped if a previous note or end is overlapping the current note body
|
||||
isOverlapping |= Precision.DefinitelyBigger(maniaPrevious.EndTime, startTime, 1) &&
|
||||
Precision.DefinitelyBigger(endTime, maniaPrevious.EndTime, 1) &&
|
||||
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1);
|
||||
|
||||
// We give a slight bonus to everything if something is held meanwhile
|
||||
if (Precision.DefinitelyBigger(maniaPrevious.EndTime, endTime, 1) &&
|
||||
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1))
|
||||
holdFactor = 1.25;
|
||||
|
||||
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - maniaPrevious.EndTime));
|
||||
}
|
||||
|
||||
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
|
||||
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
|
||||
// holdAddition
|
||||
// ^
|
||||
// 1.0 + - - - - - -+-----------
|
||||
// | /
|
||||
// 0.5 + - - - - -/ Sigmoid Curve
|
||||
// | /|
|
||||
// 0.0 +--------+-+---------------> Release Difference / ms
|
||||
// release_threshold
|
||||
if (isOverlapping)
|
||||
holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold);
|
||||
|
||||
return (1 + holdAddition) * holdFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,13 +65,22 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
{
|
||||
var sortedObjects = beatmap.HitObjects.ToArray();
|
||||
int totalColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||
|
||||
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
|
||||
|
||||
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||
List<DifficultyHitObject>[] perColumnObjects = new List<DifficultyHitObject>[totalColumns];
|
||||
|
||||
for (int column = 0; column < totalColumns; column++)
|
||||
perColumnObjects[column] = new List<DifficultyHitObject>();
|
||||
|
||||
for (int i = 1; i < sortedObjects.Length; i++)
|
||||
objects.Add(new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, objects.Count));
|
||||
{
|
||||
var currentObject = new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, perColumnObjects, objects.Count);
|
||||
objects.Add(currentObject);
|
||||
perColumnObjects[currentObject.Column].Add(currentObject);
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
@@ -12,9 +12,59 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Preprocessing
|
||||
{
|
||||
public new ManiaHitObject BaseObject => (ManiaHitObject)base.BaseObject;
|
||||
|
||||
public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List<DifficultyHitObject> objects, int index)
|
||||
private readonly List<DifficultyHitObject>[] perColumnObjects;
|
||||
|
||||
private readonly int columnIndex;
|
||||
|
||||
public readonly int Column;
|
||||
|
||||
// The hit object earlier in time than this note in each column
|
||||
public readonly ManiaDifficultyHitObject?[] PreviousHitObjects;
|
||||
|
||||
public readonly double ColumnStrainTime;
|
||||
|
||||
public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List<DifficultyHitObject> objects, List<DifficultyHitObject>[] perColumnObjects, int index)
|
||||
: base(hitObject, lastObject, clockRate, objects, index)
|
||||
{
|
||||
int totalColumns = perColumnObjects.Length;
|
||||
this.perColumnObjects = perColumnObjects;
|
||||
Column = BaseObject.Column;
|
||||
columnIndex = perColumnObjects[Column].Count;
|
||||
PreviousHitObjects = new ManiaDifficultyHitObject[totalColumns];
|
||||
ColumnStrainTime = StartTime - PrevInColumn(0)?.StartTime ?? StartTime;
|
||||
|
||||
if (index > 0)
|
||||
{
|
||||
ManiaDifficultyHitObject prevNote = (ManiaDifficultyHitObject)objects[index - 1];
|
||||
|
||||
for (int i = 0; i < prevNote.PreviousHitObjects.Length; i++)
|
||||
PreviousHitObjects[i] = prevNote.PreviousHitObjects[i];
|
||||
|
||||
// intentionally depends on processing order to match live.
|
||||
PreviousHitObjects[prevNote.Column] = prevNote;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The previous object in the same column as this <see cref="ManiaDifficultyHitObject"/>, exclusive of Long Note tails.
|
||||
/// </summary>
|
||||
/// <param name="backwardsIndex">The number of notes to go back.</param>
|
||||
/// <returns>The object in this column <paramref name="backwardsIndex"/> notes back, or null if this is the first note in the column.</returns>
|
||||
public ManiaDifficultyHitObject? PrevInColumn(int backwardsIndex)
|
||||
{
|
||||
int index = columnIndex - (backwardsIndex + 1);
|
||||
return index >= 0 && index < perColumnObjects[Column].Count ? (ManiaDifficultyHitObject)perColumnObjects[Column][index] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The next object in the same column as this <see cref="ManiaDifficultyHitObject"/>, exclusive of Long Note tails.
|
||||
/// </summary>
|
||||
/// <param name="forwardsIndex">The number of notes to go forward.</param>
|
||||
/// <returns>The object in this column <paramref name="forwardsIndex"/> notes forward, or null if this is the last note in the column.</returns>
|
||||
public ManiaDifficultyHitObject? NextInColumn(int forwardsIndex)
|
||||
{
|
||||
int index = columnIndex + (forwardsIndex + 1);
|
||||
return index >= 0 && index < perColumnObjects[Column].Count ? (ManiaDifficultyHitObject)perColumnObjects[Column][index] : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mania.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@@ -15,23 +14,17 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
{
|
||||
private const double individual_decay_base = 0.125;
|
||||
private const double overall_decay_base = 0.30;
|
||||
private const double release_threshold = 30;
|
||||
|
||||
protected override double SkillMultiplier => 1;
|
||||
protected override double StrainDecayBase => 1;
|
||||
|
||||
private readonly double[] startTimes;
|
||||
private readonly double[] endTimes;
|
||||
private readonly double[] individualStrains;
|
||||
|
||||
private double individualStrain;
|
||||
private double highestIndividualStrain;
|
||||
private double overallStrain;
|
||||
|
||||
public Strain(Mod[] mods, int totalColumns)
|
||||
: base(mods)
|
||||
{
|
||||
startTimes = new double[totalColumns];
|
||||
endTimes = new double[totalColumns];
|
||||
individualStrains = new double[totalColumns];
|
||||
overallStrain = 1;
|
||||
}
|
||||
@@ -39,65 +32,24 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||
double startTime = maniaCurrent.StartTime;
|
||||
double endTime = maniaCurrent.EndTime;
|
||||
int column = maniaCurrent.BaseObject.Column;
|
||||
bool isOverlapping = false;
|
||||
|
||||
double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information
|
||||
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
||||
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
|
||||
individualStrains[maniaCurrent.Column] = applyDecay(individualStrains[maniaCurrent.Column], maniaCurrent.ColumnStrainTime, individual_decay_base);
|
||||
individualStrains[maniaCurrent.Column] += IndividualStrainEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
for (int i = 0; i < endTimes.Length; ++i)
|
||||
{
|
||||
// The current note is overlapped if a previous note or end is overlapping the current note body
|
||||
isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) &&
|
||||
Precision.DefinitelyBigger(endTime, endTimes[i], 1) &&
|
||||
Precision.DefinitelyBigger(startTime, startTimes[i], 1);
|
||||
// Take the hardest individualStrain for notes that happen at the same time (in a chord).
|
||||
// This is to ensure the order in which the notes are processed does not affect the resultant total strain.
|
||||
highestIndividualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(highestIndividualStrain, individualStrains[maniaCurrent.Column]) : individualStrains[maniaCurrent.Column];
|
||||
|
||||
// We give a slight bonus to everything if something is held meanwhile
|
||||
if (Precision.DefinitelyBigger(endTimes[i], endTime, 1) &&
|
||||
Precision.DefinitelyBigger(startTime, startTimes[i], 1))
|
||||
holdFactor = 1.25;
|
||||
|
||||
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i]));
|
||||
}
|
||||
|
||||
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
|
||||
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
|
||||
// holdAddition
|
||||
// ^
|
||||
// 1.0 + - - - - - -+-----------
|
||||
// | /
|
||||
// 0.5 + - - - - -/ Sigmoid Curve
|
||||
// | /|
|
||||
// 0.0 +--------+-+---------------> Release Difference / ms
|
||||
// release_threshold
|
||||
if (isOverlapping)
|
||||
holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold);
|
||||
|
||||
// Decay and increase individualStrains in own column
|
||||
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);
|
||||
individualStrains[column] += 2.0 * holdFactor;
|
||||
|
||||
// For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns
|
||||
individualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(individualStrain, individualStrains[column]) : individualStrains[column];
|
||||
|
||||
// Decay and increase overallStrain
|
||||
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base);
|
||||
overallStrain += (1 + holdAddition) * holdFactor;
|
||||
|
||||
// Update startTimes and endTimes arrays
|
||||
startTimes[column] = startTime;
|
||||
endTimes[column] = endTime;
|
||||
overallStrain = applyDecay(overallStrain, maniaCurrent.DeltaTime, overall_decay_base);
|
||||
overallStrain += OverallStrainEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
// By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
|
||||
return individualStrain + overallStrain - CurrentStrain;
|
||||
return highestIndividualStrain + overallStrain - CurrentStrain;
|
||||
}
|
||||
|
||||
protected override double CalculateInitialStrain(double offset, DifficultyHitObject current)
|
||||
=> applyDecay(individualStrain, offset - current.Previous(0).StartTime, individual_decay_base)
|
||||
+ applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base);
|
||||
protected override double CalculateInitialStrain(double offset, DifficultyHitObject current) =>
|
||||
applyDecay(highestIndividualStrain, offset - current.Previous(0).StartTime, individual_decay_base)
|
||||
+ applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base);
|
||||
|
||||
private double applyDecay(double value, double deltaTime, double decayBase)
|
||||
=> value * Math.Pow(decayBase, deltaTime / 1000);
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; } = null!;
|
||||
|
||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
|
||||
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
|
||||
|
||||
public HoldNotePlacementBlueprint()
|
||||
: base(new HoldNote())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// 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 System.Text.RegularExpressions;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
@@ -54,7 +56,8 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
};
|
||||
|
||||
public override string ConvertSelectionToString()
|
||||
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}"));
|
||||
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime)
|
||||
.Select(h => FormattableString.Invariant($"{Math.Round(h.StartTime)}|{h.Column}")));
|
||||
|
||||
// 123|0,456|1,789|2 ...
|
||||
private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$", RegexOptions.Compiled);
|
||||
@@ -73,10 +76,10 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
if (split.Length != 2)
|
||||
continue;
|
||||
|
||||
if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
|
||||
if (!int.TryParse(split[0], out int time) || !int.TryParse(split[1], out int column))
|
||||
continue;
|
||||
|
||||
ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
|
||||
ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => Precision.AlmostEquals(h.StartTime, time, 0.5) && h.Column == column);
|
||||
|
||||
if (current == null)
|
||||
continue;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||