mirror of
https://github.com/ppy/osu.git
synced 2024-11-15 18:37:52 +08:00
Merge remote-tracking branch 'osumaster/master' into taikostatacc
This commit is contained in:
commit
faddc4fa99
@ -21,7 +21,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ppy.localisationanalyser.tools": {
|
"ppy.localisationanalyser.tools": {
|
||||||
"version": "2022.809.0",
|
"version": "2023.712.0",
|
||||||
"commands": [
|
"commands": [
|
||||||
"localisation"
|
"localisation"
|
||||||
]
|
]
|
||||||
|
@ -9,6 +9,9 @@ indent_style = space
|
|||||||
indent_size = 2
|
indent_size = 2
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[g_*.cs]
|
||||||
|
generated_code = true
|
||||||
|
|
||||||
[*.cs]
|
[*.cs]
|
||||||
end_of_line = crlf
|
end_of_line = crlf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -339,6 +339,5 @@ inspectcode
|
|||||||
|
|
||||||
# Fody (pulled in by Realm) - schema file
|
# Fody (pulled in by Realm) - schema file
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
**/FodyWeavers.xml
|
|
||||||
|
|
||||||
.idea/.idea.osu.Desktop/.idea/misc.xml
|
.idea/.idea.osu.Desktop/.idea/misc.xml
|
26
app.manifest
26
app.manifest
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
<assemblyIdentity version="1.0.0.0" name="osu!" />
|
<assemblyIdentity version="1.0.0.0" name="osu!" />
|
||||||
|
<SquirrelAwareVersion xmlns="urn:schema-squirrel-com:asm.v1">1</SquirrelAwareVersion>
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
<security>
|
<security>
|
||||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
@ -14,33 +15,10 @@
|
|||||||
</trustInfo>
|
</trustInfo>
|
||||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
<application>
|
<application>
|
||||||
<!-- Windows Vista -->
|
|
||||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
|
|
||||||
<!-- Windows 7 -->
|
|
||||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
|
|
||||||
<!-- Windows 8 -->
|
|
||||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
|
|
||||||
<!-- Windows 8.1 -->
|
<!-- Windows 8.1 -->
|
||||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||||
<!-- Windows 10 -->
|
<!-- Windows 10 and Windows 11 -->
|
||||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
</application>
|
</application>
|
||||||
</compatibility>
|
</compatibility>
|
||||||
<asmv3:application>
|
|
||||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
|
||||||
<dpiAware>true</dpiAware>
|
|
||||||
</asmv3:windowsSettings>
|
|
||||||
</asmv3:application>
|
|
||||||
<dependency>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity
|
|
||||||
type="win32"
|
|
||||||
name="Microsoft.Windows.Common-Controls"
|
|
||||||
version="6.0.0.0"
|
|
||||||
processorArchitecture="*"
|
|
||||||
publicKeyToken="6595b64144ccf1df"
|
|
||||||
language="*"
|
|
||||||
/>
|
|
||||||
</dependentAssembly>
|
|
||||||
</dependency>
|
|
||||||
</asmv1:assembly>
|
</asmv1:assembly>
|
||||||
|
@ -8,13 +8,9 @@
|
|||||||
<!-- NullabilityInfoContextSupport is disabled by default for Android -->
|
<!-- NullabilityInfoContextSupport is disabled by default for Android -->
|
||||||
<NullabilityInfoContextSupport>true</NullabilityInfoContextSupport>
|
<NullabilityInfoContextSupport>true</NullabilityInfoContextSupport>
|
||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.625.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.724.0" />
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<queries>
|
|
||||||
<intent>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<data android:scheme="https" />
|
|
||||||
</intent>
|
|
||||||
<intent>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<data android:scheme="mailto" />
|
|
||||||
</intent>
|
|
||||||
</queries>
|
|
||||||
</manifest>
|
|
@ -54,9 +54,6 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
client.OnReady += onReady;
|
client.OnReady += onReady;
|
||||||
|
|
||||||
// safety measure for now, until we performance test / improve backoff for failed connections.
|
|
||||||
client.OnConnectionFailed += (_, _) => client.Deinitialize();
|
|
||||||
|
|
||||||
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
|
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
|
||||||
|
|
||||||
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
|
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
|
||||||
@ -187,7 +184,7 @@ namespace osu.Desktop
|
|||||||
return edit.BeatmapInfo.ToString() ?? string.Empty;
|
return edit.BeatmapInfo.ToString() ?? string.Empty;
|
||||||
|
|
||||||
case UserActivity.WatchingReplay watching:
|
case UserActivity.WatchingReplay watching:
|
||||||
return watching.BeatmapInfo.ToString();
|
return watching.BeatmapInfo?.ToString() ?? string.Empty;
|
||||||
|
|
||||||
case UserActivity.InLobby lobby:
|
case UserActivity.InLobby lobby:
|
||||||
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;
|
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;
|
||||||
|
@ -147,14 +147,12 @@ namespace osu.Desktop
|
|||||||
{
|
{
|
||||||
base.SetHost(host);
|
base.SetHost(host);
|
||||||
|
|
||||||
var desktopWindow = (SDL2DesktopWindow)host.Window;
|
|
||||||
|
|
||||||
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||||
if (iconStream != null)
|
if (iconStream != null)
|
||||||
desktopWindow.SetIconFromStream(iconStream);
|
host.Window.SetIconFromStream(iconStream);
|
||||||
|
|
||||||
desktopWindow.CursorState |= CursorState.Hidden;
|
host.Window.CursorState |= CursorState.Hidden;
|
||||||
desktopWindow.Title = Name;
|
host.Window.Title = Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
|
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
|
||||||
<assemblyIdentity version="1.0.0.0" name="osu!" />
|
|
||||||
<SquirrelAwareVersion xmlns="urn:schema-squirrel-com:asm.v1">1</SquirrelAwareVersion>
|
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
|
||||||
<security>
|
|
||||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
|
||||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
|
||||||
</requestedPrivileges>
|
|
||||||
<applicationRequestMinimum>
|
|
||||||
<defaultAssemblyRequest permissionSetReference="Custom" />
|
|
||||||
<PermissionSet class="System.Security.PermissionSet" version="1" Unrestricted="true" ID="Custom" SameSite="site" />
|
|
||||||
</applicationRequestMinimum>
|
|
||||||
</security>
|
|
||||||
</trustInfo>
|
|
||||||
<asmv3:application>
|
|
||||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
|
||||||
<dpiAware>true</dpiAware>
|
|
||||||
</asmv3:windowsSettings>
|
|
||||||
</asmv3:application>
|
|
||||||
</asmv1:assembly>
|
|
@ -8,7 +8,6 @@
|
|||||||
<Title>osu!</Title>
|
<Title>osu!</Title>
|
||||||
<Product>osu!(lazer)</Product>
|
<Product>osu!(lazer)</Product>
|
||||||
<ApplicationIcon>lazer.ico</ApplicationIcon>
|
<ApplicationIcon>lazer.ico</ApplicationIcon>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
|
||||||
<Version>0.0.0</Version>
|
<Version>0.0.0</Version>
|
||||||
<FileVersion>0.0.0</FileVersion>
|
<FileVersion>0.0.0</FileVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@ -27,7 +26,7 @@
|
|||||||
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
|
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
|
||||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||||
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
|
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
|
||||||
<PackageReference Include="DiscordRichPresence" Version="1.1.3.18" />
|
<PackageReference Include="DiscordRichPresence" Version="1.1.4.20" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Resources">
|
<ItemGroup Label="Resources">
|
||||||
<EmbeddedResource Include="lazer.ico" />
|
<EmbeddedResource Include="lazer.ico" />
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>osu.Game.Rulesets.Catch.Tests.iOS</string>
|
<string>osu.Game.Rulesets.Catch.Tests.iOS</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>ppy.osu-Game-Rulesets-Catch-Tests-iOS</string>
|
<string>sh.ppy.catch-ruleset-tests</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
@ -42,4 +42,4 @@
|
|||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps.Legacy;
|
using osu.Game.Beatmaps.Legacy;
|
||||||
using osu.Game.Rulesets.Catch.Mods;
|
using osu.Game.Rulesets.Catch.Mods;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Tests
|
namespace osu.Game.Rulesets.Catch.Tests
|
||||||
@ -24,7 +25,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } },
|
new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } },
|
||||||
new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } },
|
new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } },
|
||||||
new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } },
|
new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } },
|
||||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }
|
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } },
|
||||||
|
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||||
};
|
};
|
||||||
|
|
||||||
[TestCaseSource(nameof(catch_mod_mapping))]
|
[TestCaseSource(nameof(catch_mod_mapping))]
|
||||||
|
@ -26,6 +26,8 @@ using osu.Game.Rulesets.Mods;
|
|||||||
using osu.Game.Rulesets.Replays.Types;
|
using osu.Game.Rulesets.Replays.Types;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Ranking.Statistics;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch
|
namespace osu.Game.Rulesets.Catch
|
||||||
@ -91,6 +93,9 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
|
|
||||||
if (mods.HasFlagFast(LegacyMods.Relax))
|
if (mods.HasFlagFast(LegacyMods.Relax))
|
||||||
yield return new CatchModRelax();
|
yield return new CatchModRelax();
|
||||||
|
|
||||||
|
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||||
|
yield return new ModScoreV2();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<Mod> GetModsFor(ModType type)
|
public override IEnumerable<Mod> GetModsFor(ModType type)
|
||||||
@ -140,6 +145,12 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
new CatchModNoScope(),
|
new CatchModNoScope(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case ModType.System:
|
||||||
|
return new Mod[]
|
||||||
|
{
|
||||||
|
new ModScoreV2(),
|
||||||
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return Array.Empty<Mod>();
|
return Array.Empty<Mod>();
|
||||||
}
|
}
|
||||||
@ -202,10 +213,24 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
|
|
||||||
public int LegacyID => 2;
|
public int LegacyID => 2;
|
||||||
|
|
||||||
|
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new CatchLegacyScoreSimulator();
|
||||||
|
|
||||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
|
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
|
||||||
|
|
||||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
||||||
|
|
||||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
|
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
|
||||||
|
|
||||||
|
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
// Todo: osu!catch should not output star rating in the 'aim' attribute.
|
// Todo: osu!catch should not output star rating in the 'aim' attribute.
|
||||||
yield return (ATTRIB_ID_AIM, StarRating);
|
yield return (ATTRIB_ID_AIM, StarRating);
|
||||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||||
@ -36,7 +35,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
|
|
||||||
StarRating = values[ATTRIB_ID_AIM];
|
StarRating = values[ATTRIB_ID_AIM];
|
||||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,9 +25,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220701;
|
public override int Version => 20220701;
|
||||||
|
|
||||||
|
private readonly IWorkingBeatmap workingBeatmap;
|
||||||
|
|
||||||
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
|
workingBeatmap = beatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
@ -38,13 +41,24 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||||
|
|
||||||
return new CatchDifficultyAttributes
|
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor,
|
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
||||||
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
|
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (ComputeLegacyScoringValues)
|
||||||
|
{
|
||||||
|
CatchLegacyScoreSimulator sv1Simulator = new CatchLegacyScoreSimulator();
|
||||||
|
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||||
|
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||||
|
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||||
|
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
|
142
osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs
Normal file
142
osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
// 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.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||||
|
{
|
||||||
|
internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator
|
||||||
|
{
|
||||||
|
public int AccuracyScore { get; private set; }
|
||||||
|
|
||||||
|
public int ComboScore { get; private set; }
|
||||||
|
|
||||||
|
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
||||||
|
|
||||||
|
private int legacyBonusScore;
|
||||||
|
private int modernBonusScore;
|
||||||
|
private int combo;
|
||||||
|
|
||||||
|
private double scoreMultiplier;
|
||||||
|
|
||||||
|
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||||
|
{
|
||||||
|
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
||||||
|
|
||||||
|
int countNormal = 0;
|
||||||
|
int countSlider = 0;
|
||||||
|
int countSpinner = 0;
|
||||||
|
|
||||||
|
foreach (HitObject obj in baseBeatmap.HitObjects)
|
||||||
|
{
|
||||||
|
switch (obj)
|
||||||
|
{
|
||||||
|
case IHasPath:
|
||||||
|
countSlider++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IHasDuration:
|
||||||
|
countSpinner++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
countNormal++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int objectCount = countNormal + countSlider + countSpinner;
|
||||||
|
|
||||||
|
int drainLength = 0;
|
||||||
|
|
||||||
|
if (baseBeatmap.HitObjects.Count > 0)
|
||||||
|
{
|
||||||
|
int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum();
|
||||||
|
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
int difficultyPeppyStars = (int)Math.Round(
|
||||||
|
(baseBeatmap.Difficulty.DrainRate
|
||||||
|
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||||
|
+ baseBeatmap.Difficulty.CircleSize
|
||||||
|
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||||
|
|
||||||
|
scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||||
|
|
||||||
|
foreach (var obj in playableBeatmap.HitObjects)
|
||||||
|
simulateHit(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void simulateHit(HitObject hitObject)
|
||||||
|
{
|
||||||
|
bool increaseCombo = true;
|
||||||
|
bool addScoreComboMultiplier = false;
|
||||||
|
|
||||||
|
bool isBonus = false;
|
||||||
|
HitResult bonusResult = HitResult.None;
|
||||||
|
|
||||||
|
int scoreIncrease = 0;
|
||||||
|
|
||||||
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
case TinyDroplet:
|
||||||
|
scoreIncrease = 10;
|
||||||
|
increaseCombo = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Droplet:
|
||||||
|
scoreIncrease = 100;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Fruit:
|
||||||
|
scoreIncrease = 300;
|
||||||
|
addScoreComboMultiplier = true;
|
||||||
|
increaseCombo = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Banana:
|
||||||
|
scoreIncrease = 1100;
|
||||||
|
increaseCombo = false;
|
||||||
|
isBonus = true;
|
||||||
|
bonusResult = HitResult.LargeBonus;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JuiceStream:
|
||||||
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
|
simulateHit(nested);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case BananaShower:
|
||||||
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
|
simulateHit(nested);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addScoreComboMultiplier)
|
||||||
|
{
|
||||||
|
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
||||||
|
ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBonus)
|
||||||
|
{
|
||||||
|
legacyBonusScore += scoreIncrease;
|
||||||
|
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
AccuracyScore += scoreIncrease;
|
||||||
|
|
||||||
|
if (increaseCombo)
|
||||||
|
combo++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,6 @@ using osu.Game.Rulesets.Catch.Objects;
|
|||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||||
{
|
{
|
||||||
@ -24,7 +23,5 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
: base(new THitObject())
|
: base(new THitObject())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Catch.UI;
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -59,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
|||||||
|
|
||||||
lastDisplayedCombo = combo;
|
lastDisplayedCombo = combo;
|
||||||
|
|
||||||
if (Time.Elapsed < 0)
|
if ((Clock as IGameplayClock)?.IsRewinding == true)
|
||||||
{
|
{
|
||||||
// needs more work to make rewind somehow look good.
|
// needs more work to make rewind somehow look good.
|
||||||
// basically we want the previous increment to play... or turning off RemoveCompletedTransforms (not feasible from a performance angle).
|
// basically we want the previous increment to play... or turning off RemoveCompletedTransforms (not feasible from a performance angle).
|
||||||
|
@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.Catch.Replays;
|
using osu.Game.Rulesets.Catch.Replays;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.UI
|
namespace osu.Game.Rulesets.Catch.UI
|
||||||
@ -96,7 +97,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
|
|
||||||
comboDisplay.X = Catcher.X;
|
comboDisplay.X = Catcher.X;
|
||||||
|
|
||||||
if (Time.Elapsed <= 0)
|
if ((Clock as IGameplayClock)?.IsRewinding == true)
|
||||||
{
|
{
|
||||||
// This is probably a wrong value, but currently the true value is not recorded.
|
// This is probably a wrong value, but currently the true value is not recorded.
|
||||||
// Setting `true` will prevent generation of false-positive after-images (with more false-negatives).
|
// Setting `true` will prevent generation of false-positive after-images (with more false-negatives).
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>osu.Game.Rulesets.Mania.Tests.iOS</string>
|
<string>osu.Game.Rulesets.Mania.Tests.iOS</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>ppy.osu-Game-Rulesets-Mania-Tests-iOS</string>
|
<string>sh.ppy.mania-ruleset-tests</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
@ -42,4 +42,4 @@
|
|||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps.Legacy;
|
using osu.Game.Beatmaps.Legacy;
|
||||||
using osu.Game.Rulesets.Mania.Mods;
|
using osu.Game.Rulesets.Mania.Mods;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Tests
|
namespace osu.Game.Rulesets.Mania.Tests
|
||||||
@ -36,7 +37,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
new object[] { LegacyMods.Key3, new[] { typeof(ManiaModKey3) } },
|
new object[] { LegacyMods.Key3, new[] { typeof(ManiaModKey3) } },
|
||||||
new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } },
|
new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } },
|
||||||
new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } },
|
new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } },
|
||||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } }
|
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } },
|
||||||
|
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||||
};
|
};
|
||||||
|
|
||||||
[TestCaseSource(nameof(mania_mod_mapping))]
|
[TestCaseSource(nameof(mania_mod_mapping))]
|
||||||
|
147
osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs
Normal file
147
osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Screens;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Replays;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mania.Replays;
|
||||||
|
using osu.Game.Rulesets.Replays;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests
|
||||||
|
{
|
||||||
|
public partial class TestSceneMaximumScore : RateAdjustedBeatmapTestScene
|
||||||
|
{
|
||||||
|
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||||
|
|
||||||
|
private List<JudgementResult> judgementResults = new List<JudgementResult>();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSimultaneousTickAndNote()
|
||||||
|
{
|
||||||
|
performTest(
|
||||||
|
new List<ManiaHitObject>
|
||||||
|
{
|
||||||
|
new HoldNote
|
||||||
|
{
|
||||||
|
StartTime = 1000,
|
||||||
|
Duration = 2000,
|
||||||
|
Column = 0,
|
||||||
|
},
|
||||||
|
new Note
|
||||||
|
{
|
||||||
|
StartTime = 2000,
|
||||||
|
Column = 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new ManiaReplayFrame(1000, ManiaAction.Key1),
|
||||||
|
new ManiaReplayFrame(2000, ManiaAction.Key1, ManiaAction.Key2),
|
||||||
|
new ManiaReplayFrame(2001, ManiaAction.Key1),
|
||||||
|
new ManiaReplayFrame(3000)
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("all objects perfectly judged",
|
||||||
|
() => judgementResults.Select(result => result.Type),
|
||||||
|
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
|
||||||
|
AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSimultaneousLongNotes()
|
||||||
|
{
|
||||||
|
performTest(
|
||||||
|
new List<ManiaHitObject>
|
||||||
|
{
|
||||||
|
new HoldNote
|
||||||
|
{
|
||||||
|
StartTime = 1000,
|
||||||
|
Duration = 2000,
|
||||||
|
Column = 0,
|
||||||
|
},
|
||||||
|
new HoldNote
|
||||||
|
{
|
||||||
|
StartTime = 2000,
|
||||||
|
Duration = 2000,
|
||||||
|
Column = 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new ManiaReplayFrame(1000, ManiaAction.Key1),
|
||||||
|
new ManiaReplayFrame(2000, ManiaAction.Key1, ManiaAction.Key2),
|
||||||
|
new ManiaReplayFrame(3000, ManiaAction.Key2),
|
||||||
|
new ManiaReplayFrame(4000)
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("all objects perfectly judged",
|
||||||
|
() => judgementResults.Select(result => result.Type),
|
||||||
|
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
|
||||||
|
AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performTest(List<ManiaHitObject> hitObjects, List<ReplayFrame> frames)
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<ManiaHitObject>
|
||||||
|
{
|
||||||
|
HitObjects = hitObjects,
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
|
||||||
|
Ruleset = new ManiaRuleset().RulesetInfo
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
|
||||||
|
|
||||||
|
AddStep("load player", () =>
|
||||||
|
{
|
||||||
|
Beatmap.Value = CreateWorkingBeatmap(beatmap);
|
||||||
|
|
||||||
|
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||||
|
|
||||||
|
p.OnLoadComplete += _ =>
|
||||||
|
{
|
||||||
|
p.ScoreProcessor.NewJudgement += result =>
|
||||||
|
{
|
||||||
|
if (currentPlayer == p) judgementResults.Add(result);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
LoadScreen(currentPlayer = p);
|
||||||
|
judgementResults = new List<JudgementResult>();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||||
|
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||||
|
|
||||||
|
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||||
|
{
|
||||||
|
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||||
|
|
||||||
|
protected override bool PauseOnFocusLost => false;
|
||||||
|
|
||||||
|
public ScoreAccessibleReplayPlayer(Score score)
|
||||||
|
: base(score, new PlayerConfiguration
|
||||||
|
{
|
||||||
|
AllowPause = false,
|
||||||
|
ShowResults = false,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
foreach (var v in base.ToDatabaseAttributes())
|
foreach (var v in base.ToDatabaseAttributes())
|
||||||
yield return v;
|
yield return v;
|
||||||
|
|
||||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
|
||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||||
}
|
}
|
||||||
@ -33,7 +32,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
{
|
{
|
||||||
base.FromDatabaseAttributes(values, onlineInfo);
|
base.FromDatabaseAttributes(values, onlineInfo);
|
||||||
|
|
||||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
|
||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||||
}
|
}
|
||||||
|
@ -31,9 +31,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220902;
|
public override int Version => 20220902;
|
||||||
|
|
||||||
|
private readonly IWorkingBeatmap workingBeatmap;
|
||||||
|
|
||||||
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
|
workingBeatmap = beatmap;
|
||||||
|
|
||||||
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
||||||
originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
|
originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
|
||||||
}
|
}
|
||||||
@ -46,15 +50,26 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
HitWindows hitWindows = new ManiaHitWindows();
|
HitWindows hitWindows = new ManiaHitWindows();
|
||||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||||
|
|
||||||
return new ManiaDifficultyAttributes
|
ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
|
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
|
||||||
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
|
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
|
||||||
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
|
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
|
||||||
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject)
|
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (ComputeLegacyScoringValues)
|
||||||
|
{
|
||||||
|
ManiaLegacyScoreSimulator sv1Simulator = new ManiaLegacyScoreSimulator();
|
||||||
|
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||||
|
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||||
|
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||||
|
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int maxComboForObject(HitObject hitObject)
|
private static int maxComboForObject(HitObject hitObject)
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Mods;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||||
|
{
|
||||||
|
internal class ManiaLegacyScoreSimulator : ILegacyScoreSimulator
|
||||||
|
{
|
||||||
|
public int AccuracyScore => 0;
|
||||||
|
public int ComboScore { get; private set; }
|
||||||
|
public double BonusScoreRatio => 0;
|
||||||
|
|
||||||
|
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||||
|
{
|
||||||
|
double multiplier = mods.Where(m => m is not (ModHidden or ModHardRock or ModDoubleTime or ModFlashlight or ManiaModFadeIn))
|
||||||
|
.Select(m => m.ScoreMultiplier)
|
||||||
|
.Aggregate(1.0, (c, n) => c * n);
|
||||||
|
|
||||||
|
ComboScore = (int)(1000000 * multiplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs
Normal file
22
osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Resources.Localisation.Web;
|
||||||
|
using osu.Game.Screens.Edit.Setup;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||||
|
{
|
||||||
|
public partial class ManiaDifficultySection : DifficultySection
|
||||||
|
{
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
CircleSizeSlider.Label = BeatmapsetsStrings.ShowStatsCsMania;
|
||||||
|
CircleSizeSlider.Description = "The number of columns in the beatmap";
|
||||||
|
if (CircleSizeSlider.Current is BindableNumber<float> circleSizeFloat)
|
||||||
|
circleSizeFloat.Precision = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -157,6 +157,9 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
|
|
||||||
if (mods.HasFlagFast(LegacyMods.Mirror))
|
if (mods.HasFlagFast(LegacyMods.Mirror))
|
||||||
yield return new ManiaModMirror();
|
yield return new ManiaModMirror();
|
||||||
|
|
||||||
|
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||||
|
yield return new ModScoreV2();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
||||||
@ -285,6 +288,12 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
new ModAdaptiveSpeed()
|
new ModAdaptiveSpeed()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case ModType.System:
|
||||||
|
return new Mod[]
|
||||||
|
{
|
||||||
|
new ModScoreV2(),
|
||||||
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return Array.Empty<Mod>();
|
return Array.Empty<Mod>();
|
||||||
}
|
}
|
||||||
@ -302,6 +311,8 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
|
|
||||||
public int LegacyID => 3;
|
public int LegacyID => 3;
|
||||||
|
|
||||||
|
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new ManiaLegacyScoreSimulator();
|
||||||
|
|
||||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame();
|
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame();
|
||||||
|
|
||||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new ManiaRulesetConfigManager(settings, RulesetInfo);
|
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new ManiaRulesetConfigManager(settings, RulesetInfo);
|
||||||
@ -401,7 +412,7 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = 250
|
Height = 250
|
||||||
}, true),
|
}, true),
|
||||||
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[]
|
||||||
{
|
{
|
||||||
new AverageHitError(score.HitEvents),
|
new AverageHitError(score.HitEvents),
|
||||||
new UnstableRate(score.HitEvents)
|
new UnstableRate(score.HitEvents)
|
||||||
@ -414,6 +425,8 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
|
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
|
||||||
|
|
||||||
|
public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum PlayfieldType
|
public enum PlayfieldType
|
||||||
|
@ -16,6 +16,7 @@ using osu.Game.Rulesets.Objects;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -298,7 +299,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
|
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
|
||||||
if (Time.Elapsed < 0)
|
if ((Clock as IGameplayClock)?.IsRewinding == true)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (CheckHittable?.Invoke(this, Time.Current) == false)
|
if (CheckHittable?.Invoke(this, Time.Current) == false)
|
||||||
@ -337,7 +338,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
|
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
|
||||||
if (Time.Elapsed < 0)
|
if ((Clock as IGameplayClock)?.IsRewinding == true)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Tail.UpdateResult();
|
Tail.UpdateResult();
|
||||||
|
@ -2,7 +2,12 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Scoring
|
namespace osu.Game.Rulesets.Mania.Scoring
|
||||||
@ -16,6 +21,9 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<HitObject> EnumerateHitObjects(IBeatmap beatmap)
|
||||||
|
=> base.EnumerateHitObjects(beatmap).OrderBy(ho => ho, JudgementOrderComparer.DEFAULT);
|
||||||
|
|
||||||
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||||
{
|
{
|
||||||
return 10000 * comboProgress
|
return 10000 * comboProgress
|
||||||
@ -25,5 +33,29 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
|||||||
|
|
||||||
protected override double GetComboScoreChange(JudgementResult result)
|
protected override double GetComboScoreChange(JudgementResult result)
|
||||||
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
|
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
|
||||||
|
|
||||||
|
private class JudgementOrderComparer : IComparer<HitObject>
|
||||||
|
{
|
||||||
|
public static readonly JudgementOrderComparer DEFAULT = new JudgementOrderComparer();
|
||||||
|
|
||||||
|
public int Compare(HitObject? x, HitObject? y)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(x, y)) return 0;
|
||||||
|
if (ReferenceEquals(x, null)) return -1;
|
||||||
|
if (ReferenceEquals(y, null)) return 1;
|
||||||
|
|
||||||
|
int result = x.GetEndTime().CompareTo(y.GetEndTime());
|
||||||
|
if (result != 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
// due to the way input is handled in mania, notes take precedence over ticks in judging order.
|
||||||
|
if (x is Note && y is not Note) return -1;
|
||||||
|
if (x is not Note && y is Note) return 1;
|
||||||
|
|
||||||
|
return x is ManiaHitObject maniaX && y is ManiaHitObject maniaY
|
||||||
|
? maniaX.Column.CompareTo(maniaY.Column)
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>osu.Game.Rulesets.Osu.Tests.iOS</string>
|
<string>osu.Game.Rulesets.Osu.Tests.iOS</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>ppy.osu-Game-Rulesets-Osu-Tests-iOS</string>
|
<string>sh.ppy.osu-ruleset-tests</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
@ -42,4 +42,4 @@
|
|||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
@ -25,6 +25,35 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
{
|
{
|
||||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSelectAfterFadedOut()
|
||||||
|
{
|
||||||
|
var slider = new Slider
|
||||||
|
{
|
||||||
|
StartTime = 0,
|
||||||
|
Position = new Vector2(100, 100),
|
||||||
|
Path = new SliderPath
|
||||||
|
{
|
||||||
|
ControlPoints =
|
||||||
|
{
|
||||||
|
new PathControlPoint(),
|
||||||
|
new PathControlPoint(new Vector2(100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||||
|
|
||||||
|
moveMouseToObject(() => slider);
|
||||||
|
|
||||||
|
AddStep("seek after end", () => EditorClock.Seek(750));
|
||||||
|
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
AddAssert("slider not selected", () => EditorBeatmap.SelectedHitObjects.Count == 0);
|
||||||
|
|
||||||
|
AddStep("seek to visible", () => EditorClock.Seek(650));
|
||||||
|
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
AddUntilStep("slider selected", () => EditorBeatmap.SelectedHitObjects.Single() == slider);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestContextMenuShownCorrectlyForSelectedSlider()
|
public void TestContextMenuShownCorrectlyForSelectedSlider()
|
||||||
{
|
{
|
||||||
|
@ -61,6 +61,20 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
assertControlPointType(0, PathType.Linear);
|
assertControlPointType(0, PathType.Linear);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlaceWithMouseMovementOutsidePlayfield()
|
||||||
|
{
|
||||||
|
addMovementStep(new Vector2(200));
|
||||||
|
addClickStep(MouseButton.Left);
|
||||||
|
|
||||||
|
AddStep("move mouse out of screen", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + Vector2.One));
|
||||||
|
addClickStep(MouseButton.Right);
|
||||||
|
|
||||||
|
assertPlaced(true);
|
||||||
|
assertControlPointCount(2);
|
||||||
|
assertControlPointType(0, PathType.Linear);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestPlaceNormalControlPoint()
|
public void TestPlaceNormalControlPoint()
|
||||||
{
|
{
|
||||||
|
@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
[TestCase("uneven-repeat-slider")]
|
[TestCase("uneven-repeat-slider")]
|
||||||
[TestCase("old-stacking")]
|
[TestCase("old-stacking")]
|
||||||
[TestCase("multi-segment-slider")]
|
[TestCase("multi-segment-slider")]
|
||||||
|
[TestCase("nan-slider")]
|
||||||
public void Test(string name) => base.Test(name);
|
public void Test(string name) => base.Test(name);
|
||||||
|
|
||||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||||
|
@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
[TestCase(6.7115569159190587d, 206, "diffcalc-test")]
|
[TestCase(6.7115569159190587d, 206, "diffcalc-test")]
|
||||||
[TestCase(1.4391311903612753d, 45, "zero-length-sliders")]
|
[TestCase(1.4391311903612753d, 45, "zero-length-sliders")]
|
||||||
|
[TestCase(0.14102693012101306d, 1, "nan-slider")]
|
||||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps.Legacy;
|
using osu.Game.Beatmaps.Legacy;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
@ -28,7 +29,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } },
|
new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } },
|
||||||
new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } },
|
new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } },
|
||||||
new object[] { LegacyMods.Target, new[] { typeof(OsuModTargetPractice) } },
|
new object[] { LegacyMods.Target, new[] { typeof(OsuModTargetPractice) } },
|
||||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }
|
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } },
|
||||||
|
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||||
};
|
};
|
||||||
|
|
||||||
[TestCaseSource(nameof(osu_mod_mapping))]
|
[TestCaseSource(nameof(osu_mod_mapping))]
|
||||||
|
@ -49,6 +49,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
AddStep("Create tests", () =>
|
AddStep("Create tests", () =>
|
||||||
{
|
{
|
||||||
|
InputTrigger triggerLeft;
|
||||||
|
InputTrigger triggerRight;
|
||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
osuInputManager = new OsuInputManager(new OsuRuleset().RulesetInfo)
|
osuInputManager = new OsuInputManager(new OsuRuleset().RulesetInfo)
|
||||||
@ -59,29 +62,39 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
leftKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.LeftButton))
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.CentreRight,
|
|
||||||
Depth = float.MinValue,
|
|
||||||
X = -100,
|
|
||||||
},
|
|
||||||
rightKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.RightButton))
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.CentreLeft,
|
|
||||||
Depth = float.MinValue,
|
|
||||||
X = 100,
|
|
||||||
},
|
|
||||||
new OsuCursorContainer
|
new OsuCursorContainer
|
||||||
{
|
{
|
||||||
Depth = float.MinValue,
|
Depth = float.MinValue,
|
||||||
|
},
|
||||||
|
triggerLeft = new TestActionKeyCounterTrigger(OsuAction.LeftButton)
|
||||||
|
{
|
||||||
|
Depth = float.MinValue
|
||||||
|
},
|
||||||
|
triggerRight = new TestActionKeyCounterTrigger(OsuAction.RightButton)
|
||||||
|
{
|
||||||
|
Depth = float.MinValue
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
new TouchVisualiser(),
|
new TouchVisualiser(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mainContent.AddRange(new[]
|
||||||
|
{
|
||||||
|
leftKeyCounter = new DefaultKeyCounter(triggerLeft)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
X = -100,
|
||||||
|
},
|
||||||
|
rightKeyCounter = new DefaultKeyCounter(triggerRight)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
X = 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,17 +214,24 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
? currSlider.Position + currSlider.Path.PositionAt(1)
|
? currSlider.Position + currSlider.Path.PositionAt(1)
|
||||||
: currHitObject.Position;
|
: currHitObject.Position;
|
||||||
|
|
||||||
|
// Note the use of `StartTime` in the code below doesn't match stable's use of `EndTime`.
|
||||||
|
// This is because in the stable implementation, `UpdateCalculations` is not called on the inner-loop hitobject (j)
|
||||||
|
// and therefore it does not have a correct `EndTime`, but instead the default of `EndTime = StartTime`.
|
||||||
|
//
|
||||||
|
// Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
|
||||||
|
// if we use `EndTime` here it would result in unexpected stacking.
|
||||||
|
|
||||||
if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance)
|
if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance)
|
||||||
{
|
{
|
||||||
currHitObject.StackHeight++;
|
currHitObject.StackHeight++;
|
||||||
startTime = beatmap.HitObjects[j].GetEndTime();
|
startTime = beatmap.HitObjects[j].StartTime;
|
||||||
}
|
}
|
||||||
else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance)
|
else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance)
|
||||||
{
|
{
|
||||||
// Case for sliders - bump notes down and right, rather than up and left.
|
// Case for sliders - bump notes down and right, rather than up and left.
|
||||||
sliderStack++;
|
sliderStack++;
|
||||||
beatmap.HitObjects[j].StackHeight -= sliderStack;
|
beatmap.HitObjects[j].StackHeight -= sliderStack;
|
||||||
startTime = beatmap.HitObjects[j].GetEndTime();
|
startTime = beatmap.HitObjects[j].StartTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
|
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
|
||||||
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
||||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
|
||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
|
|
||||||
if (ShouldSerializeFlashlightRating())
|
if (ShouldSerializeFlashlightRating())
|
||||||
@ -111,7 +110,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
||||||
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
||||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
|
||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||||
|
@ -26,9 +26,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220902;
|
public override int Version => 20220902;
|
||||||
|
|
||||||
|
private readonly IWorkingBeatmap workingBeatmap;
|
||||||
|
|
||||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
|
workingBeatmap = beatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
@ -71,7 +74,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
|
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
|
||||||
);
|
);
|
||||||
|
|
||||||
double starRating = basePerformance > 0.00001 ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
|
double starRating = basePerformance > 0.00001
|
||||||
|
? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4)
|
||||||
|
: 0;
|
||||||
|
|
||||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||||
double drainRate = beatmap.Difficulty.DrainRate;
|
double drainRate = beatmap.Difficulty.DrainRate;
|
||||||
@ -86,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||||
|
|
||||||
return new OsuDifficultyAttributes
|
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = starRating,
|
StarRating = starRating,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
@ -103,6 +108,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
SliderCount = sliderCount,
|
SliderCount = sliderCount,
|
||||||
SpinnerCount = spinnerCount,
|
SpinnerCount = spinnerCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (ComputeLegacyScoringValues)
|
||||||
|
{
|
||||||
|
OsuLegacyScoreSimulator sv1Simulator = new OsuLegacyScoreSimulator();
|
||||||
|
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||||
|
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||||
|
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||||
|
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
|
177
osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs
Normal file
177
osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
// 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.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||||
|
{
|
||||||
|
internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator
|
||||||
|
{
|
||||||
|
public int AccuracyScore { get; private set; }
|
||||||
|
|
||||||
|
public int ComboScore { get; private set; }
|
||||||
|
|
||||||
|
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
||||||
|
|
||||||
|
private int legacyBonusScore;
|
||||||
|
private int modernBonusScore;
|
||||||
|
private int combo;
|
||||||
|
|
||||||
|
private double scoreMultiplier;
|
||||||
|
private IBeatmap playableBeatmap = null!;
|
||||||
|
|
||||||
|
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||||
|
{
|
||||||
|
this.playableBeatmap = playableBeatmap;
|
||||||
|
|
||||||
|
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
||||||
|
|
||||||
|
int countNormal = 0;
|
||||||
|
int countSlider = 0;
|
||||||
|
int countSpinner = 0;
|
||||||
|
|
||||||
|
foreach (HitObject obj in workingBeatmap.Beatmap.HitObjects)
|
||||||
|
{
|
||||||
|
switch (obj)
|
||||||
|
{
|
||||||
|
case IHasPath:
|
||||||
|
countSlider++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IHasDuration:
|
||||||
|
countSpinner++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
countNormal++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int objectCount = countNormal + countSlider + countSpinner;
|
||||||
|
|
||||||
|
int drainLength = 0;
|
||||||
|
|
||||||
|
if (baseBeatmap.HitObjects.Count > 0)
|
||||||
|
{
|
||||||
|
int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum();
|
||||||
|
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
int difficultyPeppyStars = (int)Math.Round(
|
||||||
|
(baseBeatmap.Difficulty.DrainRate
|
||||||
|
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||||
|
+ baseBeatmap.Difficulty.CircleSize
|
||||||
|
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||||
|
|
||||||
|
scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||||
|
|
||||||
|
foreach (var obj in playableBeatmap.HitObjects)
|
||||||
|
simulateHit(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void simulateHit(HitObject hitObject)
|
||||||
|
{
|
||||||
|
bool increaseCombo = true;
|
||||||
|
bool addScoreComboMultiplier = false;
|
||||||
|
|
||||||
|
bool isBonus = false;
|
||||||
|
HitResult bonusResult = HitResult.None;
|
||||||
|
|
||||||
|
int scoreIncrease = 0;
|
||||||
|
|
||||||
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
case SliderHeadCircle:
|
||||||
|
case SliderTailCircle:
|
||||||
|
case SliderRepeat:
|
||||||
|
scoreIncrease = 30;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SliderTick:
|
||||||
|
scoreIncrease = 10;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpinnerBonusTick:
|
||||||
|
scoreIncrease = 1100;
|
||||||
|
increaseCombo = false;
|
||||||
|
isBonus = true;
|
||||||
|
bonusResult = HitResult.LargeBonus;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpinnerTick:
|
||||||
|
scoreIncrease = 100;
|
||||||
|
increaseCombo = false;
|
||||||
|
isBonus = true;
|
||||||
|
bonusResult = HitResult.SmallBonus;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitCircle:
|
||||||
|
scoreIncrease = 300;
|
||||||
|
addScoreComboMultiplier = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Slider:
|
||||||
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
|
simulateHit(nested);
|
||||||
|
|
||||||
|
scoreIncrease = 300;
|
||||||
|
increaseCombo = false;
|
||||||
|
addScoreComboMultiplier = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Spinner spinner:
|
||||||
|
// The spinner object applies a lenience because gameplay mechanics differ from osu-stable.
|
||||||
|
// We'll redo the calculations to match osu-stable here...
|
||||||
|
const double maximum_rotations_per_second = 477.0 / 60;
|
||||||
|
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5);
|
||||||
|
double secondsDuration = spinner.Duration / 1000;
|
||||||
|
|
||||||
|
// The total amount of half spins possible for the entire spinner.
|
||||||
|
int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2);
|
||||||
|
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
||||||
|
int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond);
|
||||||
|
// To be able to receive bonus points, the spinner must be rotated another 1.5 times.
|
||||||
|
int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3;
|
||||||
|
|
||||||
|
for (int i = 0; i <= totalHalfSpinsPossible; i++)
|
||||||
|
{
|
||||||
|
if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0)
|
||||||
|
simulateHit(new SpinnerBonusTick());
|
||||||
|
else if (i > 1 && i % 2 == 0)
|
||||||
|
simulateHit(new SpinnerTick());
|
||||||
|
}
|
||||||
|
|
||||||
|
scoreIncrease = 300;
|
||||||
|
addScoreComboMultiplier = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addScoreComboMultiplier)
|
||||||
|
{
|
||||||
|
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
||||||
|
ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBonus)
|
||||||
|
{
|
||||||
|
legacyBonusScore += scoreIncrease;
|
||||||
|
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
AccuracyScore += scoreIncrease;
|
||||||
|
|
||||||
|
if (increaseCombo)
|
||||||
|
combo++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,7 +22,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
|||||||
protected override bool AlwaysShowWhenSelected => true;
|
protected override bool AlwaysShowWhenSelected => true;
|
||||||
|
|
||||||
protected override bool ShouldBeAlive => base.ShouldBeAlive
|
protected override bool ShouldBeAlive => base.ShouldBeAlive
|
||||||
|| (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
|
|| (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime
|
||||||
|
&& editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
|
||||||
|
|
||||||
|
public override bool IsSelectable =>
|
||||||
|
// Bypass fade out extension from hit markers for selection purposes.
|
||||||
|
// This is to match stable, where even when the afterimage hit markers are still visible, objects are not selectable.
|
||||||
|
base.ShouldBeAlive;
|
||||||
|
|
||||||
protected OsuSelectionBlueprint(T hitObject)
|
protected OsuSelectionBlueprint(T hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
|
@ -62,12 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
// Give a bit of breathing room around the playfield content.
|
// Give a bit of breathing room around the playfield content.
|
||||||
PlayfieldContentContainer.Padding = new MarginPadding
|
PlayfieldContentContainer.Padding = new MarginPadding(10);
|
||||||
{
|
|
||||||
Vertical = 10,
|
|
||||||
Left = TOOLBOX_CONTRACTED_SIZE_LEFT + 10,
|
|
||||||
Right = TOOLBOX_CONTRACTED_SIZE_RIGHT + 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
LayerBelowRuleset.AddRange(new Drawable[]
|
LayerBelowRuleset.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
|
@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -179,16 +180,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
private Vector2? lastPosition;
|
private Vector2? lastPosition;
|
||||||
|
|
||||||
private bool rewinding;
|
|
||||||
|
|
||||||
public void UpdateProgress(double completionProgress)
|
public void UpdateProgress(double completionProgress)
|
||||||
{
|
{
|
||||||
Position = drawableSlider.HitObject.CurvePositionAt(completionProgress);
|
Position = drawableSlider.HitObject.CurvePositionAt(completionProgress);
|
||||||
|
|
||||||
var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f);
|
var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f);
|
||||||
|
|
||||||
if (Clock.ElapsedFrameTime != 0)
|
bool rewinding = (Clock as IGameplayClock)?.IsRewinding == true;
|
||||||
rewinding = Clock.ElapsedFrameTime < 0;
|
|
||||||
|
|
||||||
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
|
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
|
||||||
if (diff.LengthFast < 0.01f)
|
if (diff.LengthFast < 0.01f)
|
||||||
|
@ -113,6 +113,9 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
|
|
||||||
if (mods.HasFlagFast(LegacyMods.TouchDevice))
|
if (mods.HasFlagFast(LegacyMods.TouchDevice))
|
||||||
yield return new OsuModTouchDevice();
|
yield return new OsuModTouchDevice();
|
||||||
|
|
||||||
|
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||||
|
yield return new ModScoreV2();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
||||||
@ -212,6 +215,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
return new Mod[]
|
return new Mod[]
|
||||||
{
|
{
|
||||||
new OsuModTouchDevice(),
|
new OsuModTouchDevice(),
|
||||||
|
new ModScoreV2(),
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -253,6 +257,8 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
|
|
||||||
public int LegacyID => 0;
|
public int LegacyID => 0;
|
||||||
|
|
||||||
|
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new OsuLegacyScoreSimulator();
|
||||||
|
|
||||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
|
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
|
||||||
|
|
||||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
||||||
@ -313,7 +319,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = 250
|
Height = 250
|
||||||
}, true),
|
}, true),
|
||||||
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[]
|
||||||
{
|
{
|
||||||
new AverageHitError(timedHitEvents),
|
new AverageHitError(timedHitEvents),
|
||||||
new UnstableRate(timedHitEvents)
|
new UnstableRate(timedHitEvents)
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
{"Mappings":[{"StartTime":77497.0,"Objects":[{"StartTime":77497.0,"EndTime":77497.0,"X":298.0,"Y":290.0},{"StartTime":77533.0,"EndTime":77533.0,"X":276.162567,"Y":293.0336}]}]}
|
18
osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu
Executable file
18
osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
osu file format v14
|
||||||
|
|
||||||
|
[Difficulty]
|
||||||
|
HPDrainRate:5.8
|
||||||
|
CircleSize:4
|
||||||
|
OverallDifficulty:9.6
|
||||||
|
ApproachRate:10
|
||||||
|
SliderMultiplier:2
|
||||||
|
SliderTickRate:1
|
||||||
|
|
||||||
|
[TimingPoints]
|
||||||
|
77211,-100,4,3,50,70,0,0
|
||||||
|
77497,8.40402703648439,4,3,51,70,1,8
|
||||||
|
77497,NaN,4,3,51,70,0,8
|
||||||
|
77498,285.714285714286,4,3,51,70,1,0
|
||||||
|
|
||||||
|
[HitObjects]
|
||||||
|
298,290,77497,6,0,B|234:298|192:279|192:279|180:299|180:299|205:311|238:318|238:318|230:347|217:371|217:371|137:370|80:340|80:340|65:259|73:143|102:68|102:68|149:49|199:34|199:34|213:54|213:54|267:38|324:40|324:40|332:18|332:18|385:20|435:27|435:27|480:93|517:204|521:286|521:286|474:329|396:350|396:350|377:329|363:302|363:302|393:287|415:271|415:271|398:254|398:254|362:282|299:290,1,1723.66345596313,10|0,1:0|3:0,3:0:0:0:
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
@ -120,18 +121,22 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
|||||||
new OsuSpriteText
|
new OsuSpriteText
|
||||||
{
|
{
|
||||||
Text = "Overshoot",
|
Text = "Overshoot",
|
||||||
|
Font = OsuFont.GetFont(size: 12),
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.BottomCentre,
|
Origin = Anchor.BottomLeft,
|
||||||
Padding = new MarginPadding(3),
|
Padding = new MarginPadding(2),
|
||||||
|
Rotation = -rotation,
|
||||||
RelativePositionAxes = Axes.Both,
|
RelativePositionAxes = Axes.Both,
|
||||||
Y = -(inner_portion + line_extension) / 2,
|
Y = -(inner_portion + line_extension) / 2,
|
||||||
},
|
},
|
||||||
new OsuSpriteText
|
new OsuSpriteText
|
||||||
{
|
{
|
||||||
Text = "Undershoot",
|
Text = "Undershoot",
|
||||||
|
Font = OsuFont.GetFont(size: 12),
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.TopCentre,
|
Origin = Anchor.TopRight,
|
||||||
Padding = new MarginPadding(3),
|
Rotation = -rotation,
|
||||||
|
Padding = new MarginPadding(2),
|
||||||
RelativePositionAxes = Axes.Both,
|
RelativePositionAxes = Axes.Both,
|
||||||
Y = (inner_portion + line_extension) / 2,
|
Y = (inner_portion + line_extension) / 2,
|
||||||
},
|
},
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>osu.Game.Rulesets.Taiko.Tests.iOS</string>
|
<string>osu.Game.Rulesets.Taiko.Tests.iOS</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>ppy.osu-Game-Rulesets-Taiko-Tests-iOS</string>
|
<string>sh.ppy.taiko-ruleset-tests</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
@ -42,4 +42,4 @@
|
|||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -10,6 +11,7 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
@ -36,11 +38,12 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
|||||||
() => Is.EqualTo(expectedResult));
|
() => Is.EqualTo(expectedResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void PerformTest(List<ReplayFrame> frames, Beatmap<TaikoHitObject>? beatmap = null)
|
protected void PerformTest(List<ReplayFrame> frames, Beatmap<TaikoHitObject>? beatmap = null, Mod[]? mods = null)
|
||||||
{
|
{
|
||||||
AddStep("load player", () =>
|
AddStep("load player", () =>
|
||||||
{
|
{
|
||||||
Beatmap.Value = CreateWorkingBeatmap(beatmap);
|
Beatmap.Value = CreateWorkingBeatmap(beatmap);
|
||||||
|
SelectedMods.Value = mods ?? Array.Empty<Mod>();
|
||||||
|
|
||||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||||
|
|
||||||
|
@ -75,6 +75,25 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
|||||||
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
|
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitNoneStrongDrumRoll()
|
||||||
|
{
|
||||||
|
PerformTest(new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(0),
|
||||||
|
}, CreateBeatmap(createDrumRoll(true)));
|
||||||
|
|
||||||
|
AssertJudgementCount(12);
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; ++i)
|
||||||
|
{
|
||||||
|
AssertResult<DrumRollTick>(i, HitResult.IgnoreMiss);
|
||||||
|
AssertResult<DrumRollTick.StrongNestedHit>(i, HitResult.IgnoreMiss);
|
||||||
|
}
|
||||||
|
|
||||||
|
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHitAllStrongDrumRollWithOneKey()
|
public void TestHitAllStrongDrumRollWithOneKey()
|
||||||
{
|
{
|
||||||
|
@ -4,10 +4,14 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.Taiko.Mods;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Taiko.Replays;
|
using osu.Game.Rulesets.Taiko.Replays;
|
||||||
|
using osu.Game.Rulesets.Taiko.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||||
{
|
{
|
||||||
@ -32,6 +36,28 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
|||||||
AssertResult<Hit>(0, HitResult.Great);
|
AssertResult<Hit>(0, HitResult.Great);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitWithBothKeysOnSameFrameDoesNotFallThroughToNextObject()
|
||||||
|
{
|
||||||
|
PerformTest(new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(0),
|
||||||
|
new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
|
||||||
|
}, CreateBeatmap(new Hit
|
||||||
|
{
|
||||||
|
Type = HitType.Centre,
|
||||||
|
StartTime = 1000,
|
||||||
|
}, new Hit
|
||||||
|
{
|
||||||
|
Type = HitType.Centre,
|
||||||
|
StartTime = 1020
|
||||||
|
}));
|
||||||
|
|
||||||
|
AssertJudgementCount(2);
|
||||||
|
AssertResult<Hit>(0, HitResult.Great);
|
||||||
|
AssertResult<Hit>(1, HitResult.Miss);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHitRimHit()
|
public void TestHitRimHit()
|
||||||
{
|
{
|
||||||
@ -157,5 +183,58 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
|||||||
AssertJudgementCount(1);
|
AssertJudgementCount(1);
|
||||||
AssertResult<Hit>(0, HitResult.Ok);
|
AssertResult<Hit>(0, HitResult.Ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestStrongHitOneKeyWithHidden()
|
||||||
|
{
|
||||||
|
const double hit_time = 1000;
|
||||||
|
|
||||||
|
var beatmap = CreateBeatmap(new Hit
|
||||||
|
{
|
||||||
|
Type = HitType.Centre,
|
||||||
|
StartTime = hit_time,
|
||||||
|
IsStrong = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var hitWindows = new TaikoHitWindows();
|
||||||
|
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||||
|
|
||||||
|
PerformTest(new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(0),
|
||||||
|
new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre),
|
||||||
|
}, beatmap, new Mod[] { new TaikoModHidden() });
|
||||||
|
|
||||||
|
AssertJudgementCount(2);
|
||||||
|
AssertResult<Hit>(0, HitResult.Ok);
|
||||||
|
AssertResult<Hit.StrongNestedHit>(0, HitResult.IgnoreMiss);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestStrongHitTwoKeysWithHidden()
|
||||||
|
{
|
||||||
|
const double hit_time = 1000;
|
||||||
|
|
||||||
|
var beatmap = CreateBeatmap(new Hit
|
||||||
|
{
|
||||||
|
Type = HitType.Centre,
|
||||||
|
StartTime = hit_time,
|
||||||
|
IsStrong = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var hitWindows = new TaikoHitWindows();
|
||||||
|
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||||
|
|
||||||
|
PerformTest(new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(0),
|
||||||
|
new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) + DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW - 2, TaikoAction.LeftCentre, TaikoAction.RightCentre),
|
||||||
|
}, beatmap, new Mod[] { new TaikoModHidden() });
|
||||||
|
|
||||||
|
AssertJudgementCount(2);
|
||||||
|
AssertResult<Hit>(0, HitResult.Ok);
|
||||||
|
AssertResult<Hit.StrongNestedHit>(0, HitResult.LargeBonus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,5 +114,75 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
|||||||
|
|
||||||
AddAssert("all tick offsets are 0", () => JudgementResults.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0));
|
AddAssert("all tick offsets are 0", () => JudgementResults.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensure input is correctly sent to subsequent hits if a swell is fully completed.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestHitSwellThenHitHit()
|
||||||
|
{
|
||||||
|
const double swell_time = 1000;
|
||||||
|
const double hit_time = 1150;
|
||||||
|
|
||||||
|
Swell swell = new Swell
|
||||||
|
{
|
||||||
|
StartTime = swell_time,
|
||||||
|
Duration = 100,
|
||||||
|
RequiredHits = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
Hit hit = new Hit
|
||||||
|
{
|
||||||
|
StartTime = hit_time
|
||||||
|
};
|
||||||
|
|
||||||
|
List<ReplayFrame> frames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(0),
|
||||||
|
new TaikoReplayFrame(swell_time, TaikoAction.LeftRim),
|
||||||
|
new TaikoReplayFrame(hit_time, TaikoAction.RightCentre),
|
||||||
|
};
|
||||||
|
|
||||||
|
PerformTest(frames, CreateBeatmap(swell, hit));
|
||||||
|
|
||||||
|
AssertJudgementCount(3);
|
||||||
|
|
||||||
|
AssertResult<SwellTick>(0, HitResult.IgnoreHit);
|
||||||
|
AssertResult<Swell>(0, HitResult.LargeBonus);
|
||||||
|
AssertResult<Hit>(0, HitResult.Great);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMissSwellThenHitHit()
|
||||||
|
{
|
||||||
|
const double swell_time = 1000;
|
||||||
|
const double hit_time = 1150;
|
||||||
|
|
||||||
|
Swell swell = new Swell
|
||||||
|
{
|
||||||
|
StartTime = swell_time,
|
||||||
|
Duration = 100,
|
||||||
|
RequiredHits = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
Hit hit = new Hit
|
||||||
|
{
|
||||||
|
StartTime = hit_time
|
||||||
|
};
|
||||||
|
|
||||||
|
List<ReplayFrame> frames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(0),
|
||||||
|
new TaikoReplayFrame(hit_time, TaikoAction.RightCentre),
|
||||||
|
};
|
||||||
|
|
||||||
|
PerformTest(frames, CreateBeatmap(swell, hit));
|
||||||
|
|
||||||
|
AssertJudgementCount(3);
|
||||||
|
|
||||||
|
AssertResult<SwellTick>(0, HitResult.IgnoreMiss);
|
||||||
|
AssertResult<Swell>(0, HitResult.IgnoreMiss);
|
||||||
|
AssertResult<Hit>(0, HitResult.Great);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,71 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Rulesets.Taiko.Mods;
|
using osu.Game.Rulesets.Taiko.Mods;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||||
{
|
{
|
||||||
public partial class TestSceneTaikoModHidden : TaikoModTestScene
|
public partial class TestSceneTaikoModHidden : TaikoModTestScene
|
||||||
{
|
{
|
||||||
|
private Func<bool> checkAllMaxResultJudgements(int count) => ()
|
||||||
|
=> Player.ScoreProcessor.JudgedHits >= count
|
||||||
|
&& Player.Results.All(result => result.Type == result.Judgement.MaxResult);
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
|
public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
|
||||||
{
|
{
|
||||||
Mod = new TaikoModHidden(),
|
Mod = new TaikoModHidden(),
|
||||||
Autoplay = true,
|
Autoplay = true,
|
||||||
PassCondition = checkSomeAutoplayHits
|
PassCondition = checkAllMaxResultJudgements(4),
|
||||||
});
|
});
|
||||||
|
|
||||||
private bool checkSomeAutoplayHits()
|
[Test]
|
||||||
=> Player.ScoreProcessor.JudgedHits >= 4
|
public void TestHitTwoNotesWithinShortPeriod()
|
||||||
&& Player.Results.All(result => result.Type == result.Judgement.MaxResult);
|
{
|
||||||
|
const double hit_time = 1;
|
||||||
|
|
||||||
|
var beatmap = new Beatmap<TaikoHitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<TaikoHitObject>
|
||||||
|
{
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
Type = HitType.Rim,
|
||||||
|
StartTime = hit_time,
|
||||||
|
},
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
Type = HitType.Centre,
|
||||||
|
StartTime = hit_time * 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
Difficulty = new BeatmapDifficulty
|
||||||
|
{
|
||||||
|
SliderTickRate = 4,
|
||||||
|
OverallDifficulty = 0,
|
||||||
|
},
|
||||||
|
Ruleset = new TaikoRuleset().RulesetInfo
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
|
||||||
|
|
||||||
|
CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new TaikoModHidden(),
|
||||||
|
Autoplay = true,
|
||||||
|
PassCondition = checkAllMaxResultJudgements(2),
|
||||||
|
Beatmap = beatmap,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps.Legacy;
|
using osu.Game.Beatmaps.Legacy;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Taiko.Mods;
|
using osu.Game.Rulesets.Taiko.Mods;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
@ -25,7 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } },
|
new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } },
|
||||||
new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } },
|
new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } },
|
||||||
new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } },
|
new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } },
|
||||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } }
|
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } },
|
||||||
|
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||||
};
|
};
|
||||||
|
|
||||||
[TestCaseSource(nameof(taiko_mod_mapping))]
|
[TestCaseSource(nameof(taiko_mod_mapping))]
|
||||||
|
@ -72,13 +72,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
seekTo(200);
|
seekTo(200);
|
||||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -100,13 +100,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
seekTo(200);
|
seekTo(200);
|
||||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -145,23 +145,23 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
|
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
|
||||||
|
|
||||||
seekTo(120);
|
seekTo(120);
|
||||||
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
|
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
|
||||||
|
|
||||||
seekTo(480);
|
seekTo(480);
|
||||||
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
|
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
seekTo(700);
|
seekTo(700);
|
||||||
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
|
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
StartTime = 100,
|
StartTime = 100,
|
||||||
Samples = new List<HitSampleInfo>
|
Samples = new List<HitSampleInfo>
|
||||||
{
|
{
|
||||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"),
|
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM),
|
||||||
new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong
|
new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
@ -184,13 +184,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit.StrongNestedHit>);
|
AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit.StrongNestedHit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||||
|
|
||||||
seekTo(200);
|
seekTo(200);
|
||||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -213,18 +213,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
seekTo(600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
seekTo(1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -247,18 +247,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
seekTo(600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
seekTo(1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -272,8 +272,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
EndTime = 1100,
|
EndTime = 1100,
|
||||||
Samples = new List<HitSampleInfo>
|
Samples = new List<HitSampleInfo>
|
||||||
{
|
{
|
||||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"),
|
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM),
|
||||||
new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong
|
new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
@ -282,18 +282,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||||
|
|
||||||
seekTo(600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||||
|
|
||||||
seekTo(1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -319,18 +319,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
|
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
|
||||||
// But for sample playback purposes they can be ignored as noise.
|
// But for sample playback purposes they can be ignored as noise.
|
||||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
seekTo(600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
seekTo(1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -344,7 +344,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
EndTime = 1100,
|
EndTime = 1100,
|
||||||
Samples = new List<HitSampleInfo>
|
Samples = new List<HitSampleInfo>
|
||||||
{
|
{
|
||||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum")
|
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
@ -356,25 +356,26 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
|
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
|
||||||
// But for sample playback purposes they can be ignored as noise.
|
// But for sample playback purposes they can be ignored as noise.
|
||||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
|
||||||
|
|
||||||
seekTo(600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
|
||||||
|
|
||||||
seekTo(1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkSound(HitType hitType, string expectedName, string expectedBank)
|
private void checkSamples(HitType hitType, bool strong, string expectedSamplesCsv, string expectedBank)
|
||||||
{
|
{
|
||||||
AddStep($"hit {hitType}", () => triggerSource.Play(hitType));
|
AddStep($"hit {hitType}", () => triggerSource.Play(hitType, strong));
|
||||||
AddAssert($"last played sample is {expectedName}", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Name, () => Is.EqualTo(expectedName));
|
AddAssert($"last played sample is {expectedSamplesCsv}", () => string.Join(',', triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Select(s => s.Name)),
|
||||||
AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Bank, () => Is.EqualTo(expectedBank));
|
() => Is.EqualTo(expectedSamplesCsv));
|
||||||
|
AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().First().Bank, () => Is.EqualTo(expectedBank));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void seekTo(double time) => AddStep($"seek to {time}", () => gameplayClock.Seek(time));
|
private void seekTo(double time) => AddStep($"seek to {time}", () => gameplayClock.Seek(time));
|
||||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
|
|
||||||
private void addFlyingHit(HitType hitType)
|
private void addFlyingHit(HitType hitType)
|
||||||
{
|
{
|
||||||
var tick = new DrumRollTick { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current };
|
var tick = new DrumRollTick(new DrumRoll()) { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current };
|
||||||
|
|
||||||
DrawableDrumRollTick h;
|
DrawableDrumRollTick h;
|
||||||
DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType });
|
DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType });
|
||||||
|
@ -3,15 +3,16 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Audio;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Tests
|
namespace osu.Game.Rulesets.Taiko.Tests
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Taiko has some interesting rules for legacy mappings.
|
/// Taiko doesn't output any samples. They are all handled externally by <see cref="DrumSamplePlayer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HeadlessTest]
|
[HeadlessTest]
|
||||||
public partial class TestSceneSampleOutput : TestSceneTaikoPlayer
|
public partial class TestSceneSampleOutput : TestSceneTaikoPlayer
|
||||||
@ -26,10 +27,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
string.Empty,
|
string.Empty,
|
||||||
string.Empty,
|
string.Empty,
|
||||||
string.Empty,
|
string.Empty,
|
||||||
HitSampleInfo.HIT_FINISH,
|
string.Empty,
|
||||||
HitSampleInfo.HIT_WHISTLE,
|
string.Empty,
|
||||||
HitSampleInfo.HIT_WHISTLE,
|
string.Empty,
|
||||||
HitSampleInfo.HIT_WHISTLE,
|
string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
var actualSampleNames = new List<string>();
|
var actualSampleNames = new List<string>();
|
||||||
@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
|
|
||||||
AddUntilStep("all samples collected", () => actualSampleNames.Count == expectedSampleNames.Length);
|
AddUntilStep("all samples collected", () => actualSampleNames.Count == expectedSampleNames.Length);
|
||||||
|
|
||||||
AddAssert("samples are correct", () => actualSampleNames.SequenceEqual(expectedSampleNames));
|
AddAssert("samples are correct", () => actualSampleNames, () => Is.EqualTo(expectedSampleNames));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions");
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions");
|
||||||
|
@ -57,7 +57,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
foreach (var v in base.ToDatabaseAttributes())
|
foreach (var v in base.ToDatabaseAttributes())
|
||||||
yield return v;
|
yield return v;
|
||||||
|
|
||||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
|
||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||||
}
|
}
|
||||||
@ -66,7 +65,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
base.FromDatabaseAttributes(values, onlineInfo);
|
base.FromDatabaseAttributes(values, onlineInfo);
|
||||||
|
|
||||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
|
||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||||
}
|
}
|
||||||
|
@ -25,9 +25,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220902;
|
public override int Version => 20220902;
|
||||||
|
|
||||||
|
private readonly IWorkingBeatmap workingBeatmap;
|
||||||
|
|
||||||
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
|
workingBeatmap = beatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||||
@ -84,7 +87,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
HitWindows hitWindows = new TaikoHitWindows();
|
HitWindows hitWindows = new TaikoHitWindows();
|
||||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||||
|
|
||||||
return new TaikoDifficultyAttributes
|
TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = starRating,
|
StarRating = starRating,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
@ -96,6 +99,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
|
OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
|
||||||
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (ComputeLegacyScoringValues)
|
||||||
|
{
|
||||||
|
TaikoLegacyScoreSimulator sv1Simulator = new TaikoLegacyScoreSimulator();
|
||||||
|
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||||
|
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||||
|
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||||
|
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
202
osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs
Normal file
202
osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
// 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.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||||
|
{
|
||||||
|
internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator
|
||||||
|
{
|
||||||
|
public int AccuracyScore { get; private set; }
|
||||||
|
|
||||||
|
public int ComboScore { get; private set; }
|
||||||
|
|
||||||
|
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
||||||
|
|
||||||
|
private int legacyBonusScore;
|
||||||
|
private int modernBonusScore;
|
||||||
|
private int combo;
|
||||||
|
|
||||||
|
private double modMultiplier;
|
||||||
|
private int difficultyPeppyStars;
|
||||||
|
private IBeatmap playableBeatmap = null!;
|
||||||
|
private IReadOnlyList<Mod> mods = null!;
|
||||||
|
|
||||||
|
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||||
|
{
|
||||||
|
this.playableBeatmap = playableBeatmap;
|
||||||
|
this.mods = mods;
|
||||||
|
|
||||||
|
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
||||||
|
|
||||||
|
int countNormal = 0;
|
||||||
|
int countSlider = 0;
|
||||||
|
int countSpinner = 0;
|
||||||
|
|
||||||
|
foreach (HitObject obj in baseBeatmap.HitObjects)
|
||||||
|
{
|
||||||
|
switch (obj)
|
||||||
|
{
|
||||||
|
case IHasPath:
|
||||||
|
countSlider++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IHasDuration:
|
||||||
|
countSpinner++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
countNormal++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int objectCount = countNormal + countSlider + countSpinner;
|
||||||
|
|
||||||
|
int drainLength = 0;
|
||||||
|
|
||||||
|
if (baseBeatmap.HitObjects.Count > 0)
|
||||||
|
{
|
||||||
|
int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum();
|
||||||
|
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
difficultyPeppyStars = (int)Math.Round(
|
||||||
|
(baseBeatmap.Difficulty.DrainRate
|
||||||
|
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||||
|
+ baseBeatmap.Difficulty.CircleSize
|
||||||
|
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||||
|
|
||||||
|
modMultiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||||
|
|
||||||
|
foreach (var obj in playableBeatmap.HitObjects)
|
||||||
|
simulateHit(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void simulateHit(HitObject hitObject)
|
||||||
|
{
|
||||||
|
bool increaseCombo = true;
|
||||||
|
bool addScoreComboMultiplier = false;
|
||||||
|
|
||||||
|
bool isBonus = false;
|
||||||
|
HitResult bonusResult = HitResult.None;
|
||||||
|
|
||||||
|
int scoreIncrease = 0;
|
||||||
|
|
||||||
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
case SwellTick:
|
||||||
|
scoreIncrease = 300;
|
||||||
|
increaseCombo = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DrumRollTick:
|
||||||
|
scoreIncrease = 300;
|
||||||
|
increaseCombo = false;
|
||||||
|
isBonus = true;
|
||||||
|
bonusResult = HitResult.SmallBonus;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Swell swell:
|
||||||
|
// The taiko swell generally does not match the osu-stable implementation in any way.
|
||||||
|
// We'll redo the calculations to match osu-stable here...
|
||||||
|
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5);
|
||||||
|
double secondsDuration = swell.Duration / 1000;
|
||||||
|
|
||||||
|
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
||||||
|
int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond);
|
||||||
|
|
||||||
|
halfSpinsRequiredForCompletion = (int)Math.Max(1, halfSpinsRequiredForCompletion * 1.65f);
|
||||||
|
|
||||||
|
if (mods.Any(m => m is ModDoubleTime))
|
||||||
|
halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 0.75f));
|
||||||
|
if (mods.Any(m => m is ModHalfTime))
|
||||||
|
halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 1.5f));
|
||||||
|
|
||||||
|
for (int i = 0; i <= halfSpinsRequiredForCompletion; i++)
|
||||||
|
simulateHit(new SwellTick());
|
||||||
|
|
||||||
|
scoreIncrease = 300;
|
||||||
|
addScoreComboMultiplier = true;
|
||||||
|
increaseCombo = false;
|
||||||
|
isBonus = true;
|
||||||
|
bonusResult = HitResult.LargeBonus;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Hit:
|
||||||
|
scoreIncrease = 300;
|
||||||
|
addScoreComboMultiplier = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DrumRoll:
|
||||||
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
|
simulateHit(nested);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hitObject is DrumRollTick tick)
|
||||||
|
{
|
||||||
|
if (playableBeatmap.ControlPointInfo.EffectPointAt(tick.Parent.StartTime).KiaiMode)
|
||||||
|
scoreIncrease = (int)(scoreIncrease * 1.2f);
|
||||||
|
|
||||||
|
if (tick.IsStrong)
|
||||||
|
scoreIncrease += scoreIncrease / 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The score increase directly contributed to by the combo-multiplied portion.
|
||||||
|
int comboScoreIncrease = 0;
|
||||||
|
|
||||||
|
if (addScoreComboMultiplier)
|
||||||
|
{
|
||||||
|
int oldScoreIncrease = scoreIncrease;
|
||||||
|
|
||||||
|
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
||||||
|
scoreIncrease += (int)(scoreIncrease / 35 * 2 * (difficultyPeppyStars + 1) * modMultiplier) * (Math.Min(100, combo) / 10);
|
||||||
|
|
||||||
|
if (hitObject is Swell)
|
||||||
|
{
|
||||||
|
if (playableBeatmap.ControlPointInfo.EffectPointAt(hitObject.GetEndTime()).KiaiMode)
|
||||||
|
scoreIncrease = (int)(scoreIncrease * 1.2f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (playableBeatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode)
|
||||||
|
scoreIncrease = (int)(scoreIncrease * 1.2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
comboScoreIncrease = scoreIncrease - oldScoreIncrease;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hitObject is Swell || (hitObject is TaikoStrongableHitObject strongable && strongable.IsStrong))
|
||||||
|
{
|
||||||
|
scoreIncrease *= 2;
|
||||||
|
comboScoreIncrease *= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
scoreIncrease -= comboScoreIncrease;
|
||||||
|
|
||||||
|
if (addScoreComboMultiplier)
|
||||||
|
ComboScore += comboScoreIncrease;
|
||||||
|
|
||||||
|
if (isBonus)
|
||||||
|
{
|
||||||
|
legacyBonusScore += scoreIncrease;
|
||||||
|
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
AccuracyScore += scoreIncrease;
|
||||||
|
|
||||||
|
if (increaseCombo)
|
||||||
|
combo++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss;
|
effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss;
|
||||||
|
|
||||||
// TODO: The detection of rulesets is temporary until the leftover old skills have been reworked.
|
// TODO: The detection of rulesets is temporary until the leftover old skills have been reworked.
|
||||||
bool isConvert = score.BeatmapInfo.Ruleset.OnlineID != 1;
|
bool isConvert = score.BeatmapInfo!.Ruleset.OnlineID != 1;
|
||||||
|
|
||||||
double multiplier = 1.13;
|
double multiplier = 1.13;
|
||||||
|
|
||||||
|
@ -62,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
|||||||
hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged
|
hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged
|
||||||
? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss)
|
? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss)
|
||||||
: hitObject.HitStateUpdateTime;
|
: hitObject.HitStateUpdateTime;
|
||||||
|
// extend the lifetime end of the object in order to allow its nested strong hit (if any) to be judged.
|
||||||
|
hitObject.LifetimeEnd += DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -195,14 +195,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnKilled()
|
|
||||||
{
|
|
||||||
base.OnKilled();
|
|
||||||
|
|
||||||
if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged)
|
|
||||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
|
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,14 +108,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnKilled()
|
|
||||||
{
|
|
||||||
base.OnKilled();
|
|
||||||
|
|
||||||
if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged)
|
|
||||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
|
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,12 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Audio;
|
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||||
@ -37,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
|
|
||||||
private bool validActionPressed;
|
private bool validActionPressed;
|
||||||
|
|
||||||
private bool pressHandledThisFrame;
|
private double? lastPressHandleTime;
|
||||||
|
|
||||||
private readonly Bindable<HitType> type = new Bindable<HitType>();
|
private readonly Bindable<HitType> type = new Bindable<HitType>();
|
||||||
|
|
||||||
@ -78,7 +76,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
|
|
||||||
HitActions = null;
|
HitActions = null;
|
||||||
HitAction = null;
|
HitAction = null;
|
||||||
validActionPressed = pressHandledThisFrame = false;
|
validActionPressed = false;
|
||||||
|
lastPressHandleTime = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateActionsFromType()
|
private void updateActionsFromType()
|
||||||
@ -93,40 +92,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
? new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit)
|
? new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit)
|
||||||
: new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
|
: new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
|
||||||
|
|
||||||
public override IEnumerable<HitSampleInfo> GetSamples()
|
|
||||||
{
|
|
||||||
// normal and claps are always handled by the drum (see DrumSampleMapping).
|
|
||||||
// in addition, whistles are excluded as they are an alternative rim marker.
|
|
||||||
|
|
||||||
var samples = HitObject.Samples.Where(s =>
|
|
||||||
s.Name != HitSampleInfo.HIT_NORMAL
|
|
||||||
&& s.Name != HitSampleInfo.HIT_CLAP
|
|
||||||
&& s.Name != HitSampleInfo.HIT_WHISTLE);
|
|
||||||
|
|
||||||
if (HitObject.Type == HitType.Rim && HitObject.IsStrong)
|
|
||||||
{
|
|
||||||
// strong + rim always maps to whistle.
|
|
||||||
// TODO: this should really be in the legacy decoder, but can't be because legacy encoding parity would be broken.
|
|
||||||
// when we add a taiko editor, this is probably not going to play nice.
|
|
||||||
|
|
||||||
var corrected = samples.ToList();
|
|
||||||
|
|
||||||
for (int i = 0; i < corrected.Count; i++)
|
|
||||||
{
|
|
||||||
var s = corrected[i];
|
|
||||||
|
|
||||||
if (s.Name != HitSampleInfo.HIT_FINISH)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
corrected[i] = s.With(HitSampleInfo.HIT_WHISTLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return corrected;
|
|
||||||
}
|
|
||||||
|
|
||||||
return samples;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
Debug.Assert(HitObject.HitWindows != null);
|
Debug.Assert(HitObject.HitWindows != null);
|
||||||
@ -150,7 +115,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
|
|
||||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
|
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
|
||||||
{
|
{
|
||||||
if (pressHandledThisFrame)
|
if (lastPressHandleTime == Time.Current)
|
||||||
return true;
|
return true;
|
||||||
if (Judged)
|
if (Judged)
|
||||||
return false;
|
return false;
|
||||||
@ -164,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
|
|
||||||
// Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded
|
// Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded
|
||||||
// E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note
|
// E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note
|
||||||
pressHandledThisFrame = true;
|
lastPressHandleTime = Time.Current;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,15 +140,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
base.OnReleased(e);
|
base.OnReleased(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
|
||||||
{
|
|
||||||
base.Update();
|
|
||||||
|
|
||||||
// The input manager processes all input prior to us updating, so this is the perfect time
|
|
||||||
// for us to remove the extra press blocking, before input is handled in the next frame
|
|
||||||
pressHandledThisFrame = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||||
{
|
{
|
||||||
Debug.Assert(HitObject.HitWindows != null);
|
Debug.Assert(HitObject.HitWindows != null);
|
||||||
@ -231,7 +187,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
/// The lenience for the second key press.
|
/// The lenience for the second key press.
|
||||||
/// This does not adjust by map difficulty in ScoreV2 yet.
|
/// This does not adjust by map difficulty in ScoreV2 yet.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const double second_hit_window = 30;
|
public const double SECOND_HIT_WINDOW = 30;
|
||||||
|
|
||||||
public StrongNestedHit()
|
public StrongNestedHit()
|
||||||
: this(null)
|
: this(null)
|
||||||
@ -259,12 +215,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
|
|
||||||
if (!userTriggered)
|
if (!userTriggered)
|
||||||
{
|
{
|
||||||
if (timeOffset - ParentHitObject.Result.TimeOffset > second_hit_window)
|
if (timeOffset - ParentHitObject.Result.TimeOffset > SECOND_HIT_WINDOW)
|
||||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= second_hit_window)
|
if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= SECOND_HIT_WINDOW)
|
||||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Taiko.Judgements;
|
using osu.Game.Rulesets.Taiko.Judgements;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||||
@ -16,5 +17,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
: base(nestedHit)
|
: base(nestedHit)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void OnKilled()
|
||||||
|
{
|
||||||
|
base.OnKilled();
|
||||||
|
|
||||||
|
// usually, the strong nested hit isn't judged itself, it is judged by its parent object.
|
||||||
|
// however, in rare cases (see: drum rolls, hits with hidden active),
|
||||||
|
// it can happen that the hit window of the nested strong hit extends past the lifetime of the parent object.
|
||||||
|
// this is a safety to prevent such cases from causing the nested hit to never be judged and as such prevent gameplay from completing.
|
||||||
|
if (!Judged && Time.Current > ParentHitObject?.HitObject.GetEndTime())
|
||||||
|
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -276,6 +276,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
if (Time.Current < HitObject.StartTime)
|
if (Time.Current < HitObject.StartTime)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (AllJudged)
|
||||||
|
return false;
|
||||||
|
|
||||||
bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre;
|
bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre;
|
||||||
|
|
||||||
// Ensure alternating centre and rim hits
|
// Ensure alternating centre and rim hits
|
||||||
|
@ -119,8 +119,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
public override bool RemoveWhenNotAlive => false;
|
public override bool RemoveWhenNotAlive => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Most osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource).
|
// osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource).
|
||||||
public override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
|
public sealed override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract partial class DrawableTaikoHitObject<TObject> : DrawableTaikoHitObject
|
public abstract partial class DrawableTaikoHitObject<TObject> : DrawableTaikoHitObject
|
||||||
|
@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
AddNested(new DrumRollTick
|
AddNested(new DrumRollTick(this)
|
||||||
{
|
{
|
||||||
FirstTick = first,
|
FirstTick = first,
|
||||||
TickSpacing = tickSpacing,
|
TickSpacing = tickSpacing,
|
||||||
|
@ -9,6 +9,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
{
|
{
|
||||||
public class DrumRollTick : TaikoStrongableHitObject
|
public class DrumRollTick : TaikoStrongableHitObject
|
||||||
{
|
{
|
||||||
|
public readonly DrumRoll Parent;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this is the first (initial) tick of the slider.
|
/// Whether this is the first (initial) tick of the slider.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -25,6 +27,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double HitWindow => TickSpacing / 2;
|
public double HitWindow => TickSpacing / 2;
|
||||||
|
|
||||||
|
public DrumRollTick(DrumRoll parent)
|
||||||
|
{
|
||||||
|
Parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement();
|
public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement();
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||||
|
{
|
||||||
|
internal partial class ArgonDrumSamplePlayer : DrumSamplePlayer
|
||||||
|
{
|
||||||
|
private ArgonFlourishTriggerSource argonFlourishTrigger = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(Playfield playfield, IPooledSampleProvider sampleProvider)
|
||||||
|
{
|
||||||
|
var hitObjectContainer = playfield.HitObjectContainer;
|
||||||
|
|
||||||
|
// Warm up pools for non-standard samples.
|
||||||
|
sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_NORMAL), true));
|
||||||
|
sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_CLAP), true));
|
||||||
|
sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_FLOURISH), true));
|
||||||
|
|
||||||
|
// We want to play back flourishes in an isolated source as to not have them cancelled.
|
||||||
|
AddInternal(argonFlourishTrigger = new ArgonFlourishTriggerSource(hitObjectContainer));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) =>
|
||||||
|
new ArgonDrumSampleTriggerSource(hitObjectContainer, balance);
|
||||||
|
|
||||||
|
protected override void Play(DrumSampleTriggerSource triggerSource, HitType hitType, bool strong)
|
||||||
|
{
|
||||||
|
base.Play(triggerSource, hitType, strong);
|
||||||
|
|
||||||
|
// This won't always play something, but the logic for flourish playback is contained within.
|
||||||
|
argonFlourishTrigger.Play(hitType, strong);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||||
|
{
|
||||||
|
public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private ISkinSource skinSource { get; set; } = null!;
|
||||||
|
|
||||||
|
public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance)
|
||||||
|
: base(hitObjectContainer, balance)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Play(HitType hitType, bool strong)
|
||||||
|
{
|
||||||
|
TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject;
|
||||||
|
|
||||||
|
if (hitObject == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL);
|
||||||
|
|
||||||
|
// If the sample is provided by a legacy skin, we should not try and do anything special.
|
||||||
|
if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer)
|
||||||
|
{
|
||||||
|
base.Play(hitType, strong);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// let the magic begin...
|
||||||
|
var samplesToPlay = new List<ISampleInfo> { new VolumeAwareHitSampleInfo(originalSample, strong) };
|
||||||
|
|
||||||
|
PlaySamples(samplesToPlay.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||||
|
{
|
||||||
|
internal partial class ArgonFlourishTriggerSource : DrumSampleTriggerSource
|
||||||
|
{
|
||||||
|
private readonly HitObjectContainer hitObjectContainer;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ISkinSource skinSource { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The minimum time to leave between flourishes that are added to strong rim hits.
|
||||||
|
/// </summary>
|
||||||
|
private const double time_between_flourishes = 2000;
|
||||||
|
|
||||||
|
public ArgonFlourishTriggerSource(HitObjectContainer hitObjectContainer)
|
||||||
|
: base(hitObjectContainer)
|
||||||
|
{
|
||||||
|
this.hitObjectContainer = hitObjectContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Play(HitType hitType, bool strong)
|
||||||
|
{
|
||||||
|
TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject;
|
||||||
|
|
||||||
|
if (hitObject == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL);
|
||||||
|
|
||||||
|
// If the sample is provided by a legacy skin, we should not try and do anything special.
|
||||||
|
if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (strong && hitType == HitType.Rim && canPlayFlourish(hitObject))
|
||||||
|
PlaySamples(new ISampleInfo[] { new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(HitSampleInfo.HIT_FLOURISH), true) });
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool canPlayFlourish(TaikoHitObject hitObject)
|
||||||
|
{
|
||||||
|
double? lastFlourish = null;
|
||||||
|
|
||||||
|
var hitObjects = hitObjectContainer.AliveObjects
|
||||||
|
.Reverse()
|
||||||
|
.Select(d => d.HitObject)
|
||||||
|
.OfType<Hit>()
|
||||||
|
.Where(h => h.IsStrong && h.Type == HitType.Rim);
|
||||||
|
|
||||||
|
// Add an additional 'flourish' sample to strong rim hits (that are at least `time_between_flourishes` apart).
|
||||||
|
// This is applied to hitobjects in reverse order, as to sound more musically coherent by biasing towards to
|
||||||
|
// end of groups/combos of strong rim hits instead of the start.
|
||||||
|
foreach (var h in hitObjects)
|
||||||
|
{
|
||||||
|
bool canFlourish = lastFlourish == null || lastFlourish - h.StartTime >= time_between_flourishes;
|
||||||
|
|
||||||
|
if (canFlourish)
|
||||||
|
lastFlourish = h.StartTime;
|
||||||
|
|
||||||
|
// hitObject can be either the strong hit itself (if hit late), or its nested strong object (if hit early)
|
||||||
|
// due to `GetMostValidObject()` idiosyncrasies.
|
||||||
|
// whichever it is, if we encounter it during iteration, stop looking.
|
||||||
|
if (h == hitObject || h.NestedHitObjects.Contains(hitObject))
|
||||||
|
return canFlourish;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
|||||||
// the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield.
|
// the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield.
|
||||||
return Drawable.Empty().With(d => d.Expire());
|
return Drawable.Empty().With(d => d.Expire());
|
||||||
|
|
||||||
|
case TaikoSkinComponents.DrumSamplePlayer:
|
||||||
|
return new ArgonDrumSamplePlayer();
|
||||||
|
|
||||||
case TaikoSkinComponents.TaikoExplosionGreat:
|
case TaikoSkinComponents.TaikoExplosionGreat:
|
||||||
case TaikoSkinComponents.TaikoExplosionMiss:
|
case TaikoSkinComponents.TaikoExplosionMiss:
|
||||||
case TaikoSkinComponents.TaikoExplosionOk:
|
case TaikoSkinComponents.TaikoExplosionOk:
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using osu.Game.Audio;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||||
|
{
|
||||||
|
public class VolumeAwareHitSampleInfo : HitSampleInfo
|
||||||
|
{
|
||||||
|
public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90;
|
||||||
|
public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60;
|
||||||
|
|
||||||
|
public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false)
|
||||||
|
: base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<string> LookupNames
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
foreach (string name in base.LookupNames)
|
||||||
|
yield return name.Insert(name.LastIndexOf('/') + 1, "Argon/taiko-");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string getBank(string originalBank, string sampleName, int volume)
|
||||||
|
{
|
||||||
|
// So basically we're overwriting mapper's bank intentions here.
|
||||||
|
// The rationale is that most taiko beatmaps only use a single bank, but regularly adjust volume.
|
||||||
|
|
||||||
|
switch (sampleName)
|
||||||
|
{
|
||||||
|
case HIT_NORMAL:
|
||||||
|
case HIT_CLAP:
|
||||||
|
{
|
||||||
|
if (volume >= SAMPLE_VOLUME_THRESHOLD_HARD)
|
||||||
|
return BANK_DRUM;
|
||||||
|
|
||||||
|
if (volume >= SAMPLE_VOLUME_THRESHOLD_MEDIUM)
|
||||||
|
return BANK_NORMAL;
|
||||||
|
|
||||||
|
return BANK_SOFT;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return originalBank;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -8,6 +9,7 @@ using osu.Framework.Graphics.Animations;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
@ -26,11 +28,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
private Bindable<int> currentCombo { get; } = new BindableInt();
|
private Bindable<int> currentCombo { get; } = new BindableInt();
|
||||||
|
|
||||||
private int animationFrame;
|
private int animationFrame;
|
||||||
private double beatLength;
|
|
||||||
|
|
||||||
// required for editor blueprints (not sure why these circle pieces are zero size).
|
// required for editor blueprints (not sure why these circle pieces are zero size).
|
||||||
public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad;
|
public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad;
|
||||||
|
|
||||||
|
private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT;
|
||||||
|
|
||||||
public LegacyCirclePiece()
|
public LegacyCirclePiece()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
@ -39,11 +42,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private GameplayState? gameplayState { get; set; }
|
private GameplayState? gameplayState { get; set; }
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
|
||||||
private IBeatSyncProvider? beatSyncProvider { get; set; }
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ISkinSource skin, DrawableHitObject drawableHitObject)
|
private void load(ISkinSource skin, DrawableHitObject drawableHitObject, IBeatSyncProvider? beatSyncProvider)
|
||||||
{
|
{
|
||||||
Drawable? getDrawableFor(string lookup)
|
Drawable? getDrawableFor(string lookup)
|
||||||
{
|
{
|
||||||
@ -64,6 +64,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
if (foregroundLayer != null)
|
if (foregroundLayer != null)
|
||||||
AddInternal(foregroundLayer);
|
AddInternal(foregroundLayer);
|
||||||
|
|
||||||
|
drawableHitObject.StartTimeBindable.BindValueChanged(startTime =>
|
||||||
|
{
|
||||||
|
timingPoint = beatSyncProvider?.ControlPoints?.TimingPointAt(startTime.NewValue) ?? TimingControlPoint.DEFAULT;
|
||||||
|
}, true);
|
||||||
|
|
||||||
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
|
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
|
||||||
// For now just stop at first frame for sanity.
|
// For now just stop at first frame for sanity.
|
||||||
foreach (var c in InternalChildren)
|
foreach (var c in InternalChildren)
|
||||||
@ -115,14 +120,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (beatSyncProvider?.ControlPoints != null)
|
animationFrame = Math.Abs(Time.Current - timingPoint.Time) % ((timingPoint.BeatLength * 2) / multiplier) >= timingPoint.BeatLength / multiplier ? 0 : 1;
|
||||||
{
|
animatableForegroundLayer.GotoFrame(animationFrame);
|
||||||
beatLength = beatSyncProvider.ControlPoints.TimingPointAt(Time.Current).BeatLength;
|
|
||||||
|
|
||||||
animationFrame = Time.Current % ((beatLength * 2) / multiplier) >= beatLength / multiplier ? 0 : 1;
|
|
||||||
|
|
||||||
animatableForegroundLayer.GotoFrame(animationFrame);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color4 accentColour;
|
private Color4 accentColour;
|
||||||
|
@ -52,6 +52,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
case TaikoSkinComponents.DrumSamplePlayer:
|
||||||
|
return null;
|
||||||
|
|
||||||
case TaikoSkinComponents.CentreHit:
|
case TaikoSkinComponents.CentreHit:
|
||||||
case TaikoSkinComponents.RimHit:
|
case TaikoSkinComponents.RimHit:
|
||||||
if (hasHitCircle)
|
if (hasHitCircle)
|
||||||
|
@ -116,6 +116,9 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
|
|
||||||
if (mods.HasFlagFast(LegacyMods.Random))
|
if (mods.HasFlagFast(LegacyMods.Random))
|
||||||
yield return new TaikoModRandom();
|
yield return new TaikoModRandom();
|
||||||
|
|
||||||
|
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||||
|
yield return new ModScoreV2();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
||||||
@ -176,6 +179,12 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
new ModAdaptiveSpeed()
|
new ModAdaptiveSpeed()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case ModType.System:
|
||||||
|
return new Mod[]
|
||||||
|
{
|
||||||
|
new ModScoreV2(),
|
||||||
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return Array.Empty<Mod>();
|
return Array.Empty<Mod>();
|
||||||
}
|
}
|
||||||
@ -197,6 +206,8 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
|
|
||||||
public int LegacyID => 1;
|
public int LegacyID => 1;
|
||||||
|
|
||||||
|
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new TaikoLegacyScoreSimulator();
|
||||||
|
|
||||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
|
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
|
||||||
|
|
||||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TaikoRulesetConfigManager(settings, RulesetInfo);
|
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TaikoRulesetConfigManager(settings, RulesetInfo);
|
||||||
@ -245,7 +256,7 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = 250
|
Height = 250
|
||||||
}, true),
|
}, true),
|
||||||
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[]
|
||||||
{
|
{
|
||||||
new AverageHitError(timedHitEvents),
|
new AverageHitError(timedHitEvents),
|
||||||
new UnstableRate(timedHitEvents)
|
new UnstableRate(timedHitEvents)
|
||||||
|
@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
TaikoExplosionKiai,
|
TaikoExplosionKiai,
|
||||||
Scroller,
|
Scroller,
|
||||||
Mascot,
|
Mascot,
|
||||||
KiaiGlow
|
KiaiGlow,
|
||||||
|
DrumSamplePlayer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,57 +1,151 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.UI
|
namespace osu.Game.Rulesets.Taiko.UI
|
||||||
{
|
{
|
||||||
internal partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler<TaikoAction>
|
internal partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler<TaikoAction>
|
||||||
{
|
{
|
||||||
private readonly DrumSampleTriggerSource leftRimSampleTriggerSource;
|
private DrumSampleTriggerSource leftCentreTrigger = null!;
|
||||||
private readonly DrumSampleTriggerSource leftCentreSampleTriggerSource;
|
private DrumSampleTriggerSource rightCentreTrigger = null!;
|
||||||
private readonly DrumSampleTriggerSource rightCentreSampleTriggerSource;
|
private DrumSampleTriggerSource leftRimTrigger = null!;
|
||||||
private readonly DrumSampleTriggerSource rightRimSampleTriggerSource;
|
private DrumSampleTriggerSource rightRimTrigger = null!;
|
||||||
|
private DrumSampleTriggerSource strongCentreTrigger = null!;
|
||||||
|
private DrumSampleTriggerSource strongRimTrigger = null!;
|
||||||
|
|
||||||
public DrumSamplePlayer(HitObjectContainer hitObjectContainer)
|
private double lastHitTime;
|
||||||
|
private TaikoAction? lastAction;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(Playfield playfield)
|
||||||
{
|
{
|
||||||
|
var hitObjectContainer = playfield.HitObjectContainer;
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
leftRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
|
leftCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Left),
|
||||||
leftCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
|
rightCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Right),
|
||||||
rightCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
|
leftRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Left),
|
||||||
rightRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
|
rightRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Right),
|
||||||
|
strongCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Centre),
|
||||||
|
strongRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Centre)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance)
|
||||||
|
=> new DrumSampleTriggerSource(hitObjectContainer);
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
|
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
|
||||||
{
|
{
|
||||||
|
if ((Clock as IGameplayClock)?.IsRewinding == true)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
HitType hitType;
|
||||||
|
|
||||||
|
DrumSampleTriggerSource triggerSource;
|
||||||
|
|
||||||
|
bool strong = checkStrongValidity(e.Action, lastAction, Time.Current - lastHitTime);
|
||||||
|
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
case TaikoAction.LeftRim:
|
|
||||||
leftRimSampleTriggerSource.Play(HitType.Rim);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TaikoAction.LeftCentre:
|
case TaikoAction.LeftCentre:
|
||||||
leftCentreSampleTriggerSource.Play(HitType.Centre);
|
hitType = HitType.Centre;
|
||||||
|
triggerSource = strong ? strongCentreTrigger : leftCentreTrigger;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TaikoAction.RightCentre:
|
case TaikoAction.RightCentre:
|
||||||
rightCentreSampleTriggerSource.Play(HitType.Centre);
|
hitType = HitType.Centre;
|
||||||
|
triggerSource = strong ? strongCentreTrigger : rightCentreTrigger;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TaikoAction.LeftRim:
|
||||||
|
hitType = HitType.Rim;
|
||||||
|
triggerSource = strong ? strongRimTrigger : leftRimTrigger;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TaikoAction.RightRim:
|
case TaikoAction.RightRim:
|
||||||
rightRimSampleTriggerSource.Play(HitType.Rim);
|
hitType = HitType.Rim;
|
||||||
|
triggerSource = strong ? strongRimTrigger : rightRimTrigger;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (strong)
|
||||||
|
{
|
||||||
|
switch (hitType)
|
||||||
|
{
|
||||||
|
case HitType.Centre:
|
||||||
|
flushCenterTriggerSources();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitType.Rim:
|
||||||
|
flushRimTriggerSources();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Play(triggerSource, hitType, strong);
|
||||||
|
|
||||||
|
lastHitTime = Time.Current;
|
||||||
|
lastAction = e.Action;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void Play(DrumSampleTriggerSource triggerSource, HitType hitType, bool strong) =>
|
||||||
|
triggerSource.Play(hitType, strong);
|
||||||
|
|
||||||
|
private bool checkStrongValidity(TaikoAction newAction, TaikoAction? lastAction, double timeBetweenActions)
|
||||||
|
{
|
||||||
|
if (lastAction == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (timeBetweenActions < 0 || timeBetweenActions > DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
switch (newAction)
|
||||||
|
{
|
||||||
|
case TaikoAction.LeftCentre:
|
||||||
|
return lastAction == TaikoAction.RightCentre;
|
||||||
|
|
||||||
|
case TaikoAction.RightCentre:
|
||||||
|
return lastAction == TaikoAction.LeftCentre;
|
||||||
|
|
||||||
|
case TaikoAction.LeftRim:
|
||||||
|
return lastAction == TaikoAction.RightRim;
|
||||||
|
|
||||||
|
case TaikoAction.RightRim:
|
||||||
|
return lastAction == TaikoAction.LeftRim;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushCenterTriggerSources()
|
||||||
|
{
|
||||||
|
leftCentreTrigger.StopAllPlayback();
|
||||||
|
rightCentreTrigger.StopAllPlayback();
|
||||||
|
strongCentreTrigger.StopAllPlayback();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushRimTriggerSources()
|
||||||
|
{
|
||||||
|
leftRimTrigger.StopAllPlayback();
|
||||||
|
rightRimTrigger.StopAllPlayback();
|
||||||
|
strongRimTrigger.StopAllPlayback();
|
||||||
|
}
|
||||||
|
|
||||||
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
|
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -2,30 +2,74 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.UI
|
namespace osu.Game.Rulesets.Taiko.UI
|
||||||
{
|
{
|
||||||
public partial class DrumSampleTriggerSource : GameplaySampleTriggerSource
|
public partial class DrumSampleTriggerSource : GameplaySampleTriggerSource
|
||||||
{
|
{
|
||||||
public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer)
|
private const double stereo_separation = 0.2;
|
||||||
|
|
||||||
|
public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance = SampleBalance.Centre)
|
||||||
: base(hitObjectContainer)
|
: base(hitObjectContainer)
|
||||||
{
|
{
|
||||||
|
switch (balance)
|
||||||
|
{
|
||||||
|
case SampleBalance.Left:
|
||||||
|
AudioContainer.Balance.Value = -stereo_separation;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SampleBalance.Centre:
|
||||||
|
AudioContainer.Balance.Value = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SampleBalance.Right:
|
||||||
|
AudioContainer.Balance.Value = stereo_separation;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Play(HitType hitType)
|
public virtual void Play(HitType hitType, bool strong)
|
||||||
{
|
{
|
||||||
var hitSample = GetMostValidObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
|
TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject;
|
||||||
|
|
||||||
if (hitSample == null)
|
if (hitObject == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PlaySamples(new ISampleInfo[] { new HitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL, hitSample.Bank, volume: hitSample.Volume) });
|
var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL);
|
||||||
|
|
||||||
|
if (strong)
|
||||||
|
{
|
||||||
|
PlaySamples(new ISampleInfo[]
|
||||||
|
{
|
||||||
|
baseSample,
|
||||||
|
hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PlaySamples(new ISampleInfo[] { baseSample });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");
|
public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");
|
||||||
|
|
||||||
|
protected override void ApplySampleInfo(SkinnableSound hitSound, ISampleInfo[] samples)
|
||||||
|
{
|
||||||
|
base.ApplySampleInfo(hitSound, samples);
|
||||||
|
|
||||||
|
hitSound.Balance.Value = -0.05 + RNG.NextDouble(0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SampleBalance
|
||||||
|
{
|
||||||
|
Left,
|
||||||
|
Centre,
|
||||||
|
Right
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,7 +170,10 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
drumRollHitContainer.CreateProxy(),
|
drumRollHitContainer.CreateProxy(),
|
||||||
new DrumSamplePlayer(HitObjectContainer),
|
new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumSamplePlayer), _ => new DrumSamplePlayer())
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
// this is added at the end of the hierarchy to receive input before taiko objects.
|
// this is added at the end of the hierarchy to receive input before taiko objects.
|
||||||
// but is proxied below everything to not cover visual effects such as hit explosions.
|
// but is proxied below everything to not cover visual effects such as hit explosions.
|
||||||
inputDrum,
|
inputDrum,
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>osu.Game.Tests.iOS</string>
|
<string>osu.Game.Tests.iOS</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>ppy.osu-Game-Tests-iOS</string>
|
<string>sh.ppy.osu-tests</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
@ -42,4 +42,4 @@
|
|||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
@ -64,7 +64,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCachedRetrievalWithFiles() => AddStep("run test", () =>
|
public void TestCachedRetrievalWithFiles() => AddStep("run test", () =>
|
||||||
{
|
{
|
||||||
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
|
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID)!.Detach());
|
||||||
|
|
||||||
Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0));
|
Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0));
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestForcedRefetchRetrievalWithFiles() => AddStep("run test", () =>
|
public void TestForcedRefetchRetrievalWithFiles() => AddStep("run test", () =>
|
||||||
{
|
{
|
||||||
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
|
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID)!.Detach());
|
||||||
|
|
||||||
Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0));
|
Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0));
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestSavePreservesCollections() => AddStep("run test", () =>
|
public void TestSavePreservesCollections() => AddStep("run test", () =>
|
||||||
{
|
{
|
||||||
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
|
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID)!.Detach());
|
||||||
|
|
||||||
var working = beatmaps.GetWorkingBeatmap(beatmap);
|
var working = beatmaps.GetWorkingBeatmap(beatmap);
|
||||||
|
|
||||||
|
@ -478,8 +478,8 @@ namespace osu.Game.Tests.Chat
|
|||||||
Content = "This is a [http://www.simple-test.com simple test] with some [traps] and [[wiki links]]. Don't forget to visit https://osu.ppy.sh (now!)[http://google.com]\uD83D\uDE12"
|
Content = "This is a [http://www.simple-test.com simple test] with some [traps] and [[wiki links]]. Don't forget to visit https://osu.ppy.sh (now!)[http://google.com]\uD83D\uDE12"
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!\0\0\0", result.DisplayContent);
|
Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now![emoji]", result.DisplayContent);
|
||||||
Assert.AreEqual(5, result.Links.Count);
|
Assert.AreEqual(4, result.Links.Count);
|
||||||
|
|
||||||
Link f = result.Links.Find(l => l.Url == "https://dev.ppy.sh/wiki/wiki links");
|
Link f = result.Links.Find(l => l.Url == "https://dev.ppy.sh/wiki/wiki links");
|
||||||
Assert.That(f, Is.Not.Null);
|
Assert.That(f, Is.Not.Null);
|
||||||
@ -500,27 +500,22 @@ namespace osu.Game.Tests.Chat
|
|||||||
Assert.That(f, Is.Not.Null);
|
Assert.That(f, Is.Not.Null);
|
||||||
Assert.AreEqual(78, f.Index);
|
Assert.AreEqual(78, f.Index);
|
||||||
Assert.AreEqual(18, f.Length);
|
Assert.AreEqual(18, f.Length);
|
||||||
|
|
||||||
f = result.Links.Find(l => l.Url == "\uD83D\uDE12");
|
|
||||||
Assert.That(f, Is.Not.Null);
|
|
||||||
Assert.AreEqual(101, f.Index);
|
|
||||||
Assert.AreEqual(3, f.Length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestEmoji()
|
public void TestEmoji()
|
||||||
{
|
{
|
||||||
Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" });
|
Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more emojis among us:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" });
|
||||||
Assert.AreEqual("Hello world\0\0\0<--This is an emoji,There are more:\0\0\0\0\0\0,\0\0\0", result.DisplayContent);
|
Assert.AreEqual("Hello world[emoji]<--This is an emoji,There are more emojis among us:[emoji][emoji],[emoji]", result.DisplayContent);
|
||||||
Assert.AreEqual(result.Links.Count, 4);
|
Assert.AreEqual(result.Links.Count, 0);
|
||||||
Assert.AreEqual(result.Links[0].Index, 11);
|
}
|
||||||
Assert.AreEqual(result.Links[1].Index, 49);
|
|
||||||
Assert.AreEqual(result.Links[2].Index, 52);
|
[Test]
|
||||||
Assert.AreEqual(result.Links[3].Index, 56);
|
public void TestEmojiWithSuccessiveParens()
|
||||||
Assert.AreEqual(result.Links[0].Url, "\uD83D\uDE12");
|
{
|
||||||
Assert.AreEqual(result.Links[1].Url, "\uD83D\uDE10");
|
Message result = MessageFormatter.FormatMessage(new Message { Content = "\uD83D\uDE10(let's hope this doesn't accidentally turn into a link)" });
|
||||||
Assert.AreEqual(result.Links[2].Url, "\uD83D\uDE00");
|
Assert.AreEqual("[emoji](let's hope this doesn't accidentally turn into a link)", result.DisplayContent);
|
||||||
Assert.AreEqual(result.Links[3].Url, "\uD83D\uDE20");
|
Assert.AreEqual(result.Links.Count, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
return Realm.Run(r =>
|
return Realm.Run(r =>
|
||||||
{
|
{
|
||||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -51,7 +51,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
Realm.Write(r =>
|
Realm.Write(r =>
|
||||||
{
|
{
|
||||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||||
foreach (var b in beatmapSetInfo.Beatmaps)
|
foreach (var b in beatmapSetInfo.Beatmaps)
|
||||||
b.StarRating = -1;
|
b.StarRating = -1;
|
||||||
});
|
});
|
||||||
@ -66,7 +66,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
return Realm.Run(r =>
|
return Realm.Run(r =>
|
||||||
{
|
{
|
||||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -79,7 +79,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
return Realm.Run(r =>
|
return Realm.Run(r =>
|
||||||
{
|
{
|
||||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -90,7 +90,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
Realm.Write(r =>
|
Realm.Write(r =>
|
||||||
{
|
{
|
||||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||||
foreach (var b in beatmapSetInfo.Beatmaps)
|
foreach (var b in beatmapSetInfo.Beatmaps)
|
||||||
b.StarRating = -1;
|
b.StarRating = -1;
|
||||||
});
|
});
|
||||||
@ -107,7 +107,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
return Realm.Run(r =>
|
return Realm.Run(r =>
|
||||||
{
|
{
|
||||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating == -1);
|
return beatmapSetInfo.Beatmaps.All(b => b.StarRating == -1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
return Realm.Run(r =>
|
return Realm.Run(r =>
|
||||||
{
|
{
|
||||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,7 @@ using osu.Game.Extensions;
|
|||||||
using osu.Game.Models;
|
using osu.Game.Models;
|
||||||
using osu.Game.Overlays.Notifications;
|
using osu.Game.Overlays.Notifications;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using Realms;
|
using Realms;
|
||||||
using SharpCompress.Archives;
|
using SharpCompress.Archives;
|
||||||
@ -416,6 +417,108 @@ namespace osu.Game.Tests.Database
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImport_Modify_Revert()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var store = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||||
|
|
||||||
|
await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First());
|
||||||
|
|
||||||
|
var score = realm.Run(r => r.All<ScoreInfo>().Single());
|
||||||
|
|
||||||
|
string originalHash = imported.Beatmaps.First().Hash;
|
||||||
|
const string modified_hash = "new_hash";
|
||||||
|
|
||||||
|
Assert.That(imported.Beatmaps.First().Scores.Single(), Is.EqualTo(score));
|
||||||
|
|
||||||
|
Assert.That(score.BeatmapHash, Is.EqualTo(originalHash));
|
||||||
|
Assert.That(score.BeatmapInfo, Is.EqualTo(imported.Beatmaps.First()));
|
||||||
|
|
||||||
|
// imitate making local changes via editor
|
||||||
|
// ReSharper disable once MethodHasAsyncOverload
|
||||||
|
realm.Write(r =>
|
||||||
|
{
|
||||||
|
BeatmapInfo beatmap = imported.Beatmaps.First();
|
||||||
|
beatmap.Hash = modified_hash;
|
||||||
|
beatmap.ResetOnlineInfo();
|
||||||
|
beatmap.UpdateLocalScores(r);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(!imported.Beatmaps.First().Scores.Any());
|
||||||
|
|
||||||
|
Assert.That(score.BeatmapInfo, Is.Null);
|
||||||
|
Assert.That(score.BeatmapHash, Is.EqualTo(originalHash));
|
||||||
|
|
||||||
|
// imitate reverting the local changes made above
|
||||||
|
// ReSharper disable once MethodHasAsyncOverload
|
||||||
|
realm.Write(r =>
|
||||||
|
{
|
||||||
|
BeatmapInfo beatmap = imported.Beatmaps.First();
|
||||||
|
beatmap.Hash = originalHash;
|
||||||
|
beatmap.ResetOnlineInfo();
|
||||||
|
beatmap.UpdateLocalScores(r);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(imported.Beatmaps.First().Scores.Single(), Is.EqualTo(score));
|
||||||
|
|
||||||
|
Assert.That(score.BeatmapHash, Is.EqualTo(originalHash));
|
||||||
|
Assert.That(score.BeatmapInfo, Is.EqualTo(imported.Beatmaps.First()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImport_ThenModifyMapWithScore_ThenImport()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var store = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
string? temp = TestResources.GetTestBeatmapForImport();
|
||||||
|
|
||||||
|
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||||
|
|
||||||
|
await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First());
|
||||||
|
|
||||||
|
Assert.That(imported.Beatmaps.First().Scores.Any());
|
||||||
|
|
||||||
|
// imitate making local changes via editor
|
||||||
|
// ReSharper disable once MethodHasAsyncOverload
|
||||||
|
realm.Write(r =>
|
||||||
|
{
|
||||||
|
BeatmapInfo beatmap = imported.Beatmaps.First();
|
||||||
|
beatmap.Hash = "new_hash";
|
||||||
|
beatmap.ResetOnlineInfo();
|
||||||
|
beatmap.UpdateLocalScores(r);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(!imported.Beatmaps.First().Scores.Any());
|
||||||
|
|
||||||
|
var importedSecondTime = await importer.Import(new ImportTask(temp));
|
||||||
|
|
||||||
|
EnsureLoaded(realm.Realm);
|
||||||
|
|
||||||
|
// check the newly "imported" beatmap is not the original.
|
||||||
|
Assert.NotNull(importedSecondTime);
|
||||||
|
Debug.Assert(importedSecondTime != null);
|
||||||
|
Assert.That(imported.ID != importedSecondTime.ID);
|
||||||
|
|
||||||
|
var importedFirstTimeBeatmap = imported.Beatmaps.First();
|
||||||
|
var importedSecondTimeBeatmap = importedSecondTime.PerformRead(s => s.Beatmaps.First());
|
||||||
|
|
||||||
|
Assert.That(importedFirstTimeBeatmap.ID != importedSecondTimeBeatmap.ID);
|
||||||
|
Assert.That(importedFirstTimeBeatmap.Hash != importedSecondTimeBeatmap.Hash);
|
||||||
|
Assert.That(!importedFirstTimeBeatmap.Scores.Any());
|
||||||
|
Assert.That(importedSecondTimeBeatmap.Scores.Count() == 1);
|
||||||
|
Assert.That(importedSecondTimeBeatmap.Scores.Single().BeatmapInfo, Is.EqualTo(importedSecondTimeBeatmap));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestImportThenImportWithChangedFile()
|
public void TestImportThenImportWithChangedFile()
|
||||||
{
|
{
|
||||||
@ -1074,18 +1177,16 @@ namespace osu.Game.Tests.Database
|
|||||||
Assert.IsTrue(realm.All<BeatmapSetInfo>().First(_ => true).DeletePending);
|
Assert.IsTrue(realm.All<BeatmapSetInfo>().First(_ => true).DeletePending);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task createScoreForBeatmap(Realm realm, BeatmapInfo beatmap)
|
private static Task createScoreForBeatmap(Realm realm, BeatmapInfo beatmap) =>
|
||||||
{
|
realm.WriteAsync(() =>
|
||||||
// TODO: reimplement when we have score support in realm.
|
{
|
||||||
// return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
|
realm.Add(new ScoreInfo
|
||||||
// {
|
{
|
||||||
// OnlineID = 2,
|
OnlineID = 2,
|
||||||
// Beatmap = beatmap,
|
BeatmapInfo = beatmap,
|
||||||
// BeatmapInfoID = beatmap.ID
|
BeatmapHash = beatmap.Hash
|
||||||
// }, new ImportScoreTest.TestArchiveReader());
|
});
|
||||||
|
});
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false)
|
private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false)
|
||||||
{
|
{
|
||||||
|
@ -323,7 +323,7 @@ namespace osu.Game.Tests.Database
|
|||||||
var beatmapInfo = s.Beatmaps.First(b => b.File?.Filename != removedFilename);
|
var beatmapInfo = s.Beatmaps.First(b => b.File?.Filename != removedFilename);
|
||||||
|
|
||||||
scoreTargetBeatmapHash = beatmapInfo.Hash;
|
scoreTargetBeatmapHash = beatmapInfo.Hash;
|
||||||
s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
|
s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
|
||||||
});
|
});
|
||||||
|
|
||||||
realm.Run(r => r.Refresh());
|
realm.Run(r => r.Refresh());
|
||||||
@ -347,6 +347,73 @@ namespace osu.Game.Tests.Database
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDanglingScoreTransferred()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var rulesets = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
using var __ = getBeatmapArchive(out string pathOriginal);
|
||||||
|
using var _ = getBeatmapArchive(out string pathOnlineCopy);
|
||||||
|
|
||||||
|
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importBeforeUpdate != null);
|
||||||
|
|
||||||
|
string scoreTargetBeatmapHash = string.Empty;
|
||||||
|
|
||||||
|
// set a score on the beatmap
|
||||||
|
importBeforeUpdate.PerformWrite(s =>
|
||||||
|
{
|
||||||
|
var beatmapInfo = s.Beatmaps.First();
|
||||||
|
|
||||||
|
scoreTargetBeatmapHash = beatmapInfo.Hash;
|
||||||
|
|
||||||
|
s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// locally modify beatmap
|
||||||
|
const string new_beatmap_hash = "new_hash";
|
||||||
|
importBeforeUpdate.PerformWrite(s =>
|
||||||
|
{
|
||||||
|
var beatmapInfo = s.Beatmaps.First(b => b.Hash == scoreTargetBeatmapHash);
|
||||||
|
|
||||||
|
beatmapInfo.Hash = new_beatmap_hash;
|
||||||
|
beatmapInfo.ResetOnlineInfo();
|
||||||
|
beatmapInfo.UpdateLocalScores(s.Realm!);
|
||||||
|
});
|
||||||
|
|
||||||
|
realm.Run(r => r.Refresh());
|
||||||
|
|
||||||
|
// making changes to a beatmap doesn't remove the score from realm, but should disassociate the beatmap.
|
||||||
|
checkCount<ScoreInfo>(realm, 1);
|
||||||
|
Assert.That(realm.Run(r => r.All<ScoreInfo>().First().BeatmapInfo), Is.Null);
|
||||||
|
|
||||||
|
// reimport the original beatmap before local modifications
|
||||||
|
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOnlineCopy), importBeforeUpdate.Value);
|
||||||
|
|
||||||
|
Assert.That(importAfterUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importAfterUpdate != null);
|
||||||
|
|
||||||
|
realm.Run(r => r.Refresh());
|
||||||
|
|
||||||
|
// both original and locally modified versions present
|
||||||
|
checkCount<BeatmapInfo>(realm, count_beatmaps + 1);
|
||||||
|
checkCount<BeatmapMetadata>(realm, count_beatmaps + 1);
|
||||||
|
checkCount<BeatmapSetInfo>(realm, 2);
|
||||||
|
|
||||||
|
// score is preserved
|
||||||
|
checkCount<ScoreInfo>(realm, 1);
|
||||||
|
|
||||||
|
// score is transferred to new beatmap
|
||||||
|
Assert.That(importBeforeUpdate.Value.Beatmaps.First(b => b.Hash == new_beatmap_hash).Scores, Has.Count.EqualTo(0));
|
||||||
|
Assert.That(importAfterUpdate.Value.Beatmaps.First(b => b.Hash == scoreTargetBeatmapHash).Scores, Has.Count.EqualTo(1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestScoreLostOnModification()
|
public void TestScoreLostOnModification()
|
||||||
{
|
{
|
||||||
@ -368,7 +435,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
var beatmapInfo = s.Beatmaps.Last();
|
var beatmapInfo = s.Beatmaps.Last();
|
||||||
scoreTargetFilename = beatmapInfo.File?.Filename;
|
scoreTargetFilename = beatmapInfo.File?.Filename;
|
||||||
s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
|
s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
|
||||||
});
|
});
|
||||||
|
|
||||||
realm.Run(r => r.Refresh());
|
realm.Run(r => r.Refresh());
|
||||||
@ -461,7 +528,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
importBeforeUpdate.PerformWrite(s =>
|
importBeforeUpdate.PerformWrite(s =>
|
||||||
{
|
{
|
||||||
var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection"));
|
var beatmapCollection = s.Realm!.Add(new BeatmapCollection("test collection"));
|
||||||
beatmapsToAddToCollection = s.Beatmaps.Count - (allOriginalBeatmapsInCollection ? 0 : 1);
|
beatmapsToAddToCollection = s.Beatmaps.Count - (allOriginalBeatmapsInCollection ? 0 : 1);
|
||||||
|
|
||||||
for (int i = 0; i < beatmapsToAddToCollection; i++)
|
for (int i = 0; i < beatmapsToAddToCollection; i++)
|
||||||
@ -476,7 +543,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
importAfterUpdate.PerformRead(updated =>
|
importAfterUpdate.PerformRead(updated =>
|
||||||
{
|
{
|
||||||
updated.Realm.Refresh();
|
updated.Realm!.Refresh();
|
||||||
|
|
||||||
string[] hashes = updated.Realm.All<BeatmapCollection>().Single().BeatmapMD5Hashes.ToArray();
|
string[] hashes = updated.Realm.All<BeatmapCollection>().Single().BeatmapMD5Hashes.ToArray();
|
||||||
|
|
||||||
@ -526,7 +593,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
importBeforeUpdate.PerformWrite(s =>
|
importBeforeUpdate.PerformWrite(s =>
|
||||||
{
|
{
|
||||||
var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection"));
|
var beatmapCollection = s.Realm!.Add(new BeatmapCollection("test collection"));
|
||||||
originalHash = s.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash;
|
originalHash = s.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash;
|
||||||
|
|
||||||
beatmapCollection.BeatmapMD5Hashes.Add(originalHash);
|
beatmapCollection.BeatmapMD5Hashes.Add(originalHash);
|
||||||
@ -540,7 +607,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
importAfterUpdate.PerformRead(updated =>
|
importAfterUpdate.PerformRead(updated =>
|
||||||
{
|
{
|
||||||
updated.Realm.Refresh();
|
updated.Realm!.Refresh();
|
||||||
|
|
||||||
string[] hashes = updated.Realm.All<BeatmapCollection>().Single().BeatmapMD5Hashes.ToArray();
|
string[] hashes = updated.Realm.All<BeatmapCollection>().Single().BeatmapMD5Hashes.ToArray();
|
||||||
string updatedHash = updated.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash;
|
string updatedHash = updated.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash;
|
||||||
|
@ -128,7 +128,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
realm.RegisterCustomSubscription(r =>
|
realm.RegisterCustomSubscription(r =>
|
||||||
{
|
{
|
||||||
var subscription = r.All<BeatmapInfo>().QueryAsyncWithNotifications((_, _, _) =>
|
var subscription = r.All<BeatmapInfo>().QueryAsyncWithNotifications((_, _) =>
|
||||||
{
|
{
|
||||||
realm.Run(_ =>
|
realm.Run(_ =>
|
||||||
{
|
{
|
||||||
|
@ -355,7 +355,7 @@ namespace osu.Game.Tests.Database
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
void gotChange(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error)
|
void gotChange(IRealmCollection<BeatmapInfo> sender, ChangeSet? changes)
|
||||||
{
|
{
|
||||||
changesTriggered++;
|
changesTriggered++;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -54,7 +53,7 @@ namespace osu.Game.Tests.Database
|
|||||||
registration.Dispose();
|
registration.Dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error)
|
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
|
||||||
{
|
{
|
||||||
lastChanges = changes;
|
lastChanges = changes;
|
||||||
|
|
||||||
@ -92,7 +91,7 @@ namespace osu.Game.Tests.Database
|
|||||||
registration.Dispose();
|
registration.Dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error) => lastChanges = changes;
|
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes) => lastChanges = changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -185,7 +184,7 @@ namespace osu.Game.Tests.Database
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error)
|
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
|
||||||
{
|
{
|
||||||
if (changes == null)
|
if (changes == null)
|
||||||
resolvedItems = sender;
|
resolvedItems = sender;
|
||||||
|
@ -76,12 +76,12 @@ namespace osu.Game.Tests.Database
|
|||||||
Available = true,
|
Available = true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.True);
|
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
|
||||||
|
|
||||||
// Availability is updated on construction of a RealmRulesetStore
|
// Availability is updated on construction of a RealmRulesetStore
|
||||||
var _ = new RealmRulesetStore(realm, storage);
|
var _ = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.False);
|
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.False);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,18 +101,18 @@ namespace osu.Game.Tests.Database
|
|||||||
Available = true,
|
Available = true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.True);
|
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
|
||||||
|
|
||||||
// Availability is updated on construction of a RealmRulesetStore
|
// Availability is updated on construction of a RealmRulesetStore
|
||||||
var _ = new RealmRulesetStore(realm, storage);
|
var _ = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.False);
|
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.False);
|
||||||
|
|
||||||
// Simulate the ruleset getting updated
|
// Simulate the ruleset getting updated
|
||||||
LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION;
|
LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION;
|
||||||
var __ = new RealmRulesetStore(realm, storage);
|
var __ = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.True);
|
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
realm.Run(innerRealm =>
|
realm.Run(innerRealm =>
|
||||||
{
|
{
|
||||||
var binding = innerRealm.ResolveReference(tsr);
|
var binding = innerRealm.ResolveReference(tsr)!;
|
||||||
innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
|
innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
183
osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs
Normal file
183
osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.Timing;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Edit.Checks;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Editing.Checks
|
||||||
|
{
|
||||||
|
public class CheckBreaksTest
|
||||||
|
{
|
||||||
|
private CheckBreaks check = null!;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
check = new CheckBreaks();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBreakTooShort()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
Breaks = new List<BreakPeriod>
|
||||||
|
{
|
||||||
|
new BreakPeriod(0, 649)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||||
|
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateTooShort);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBreakStartsEarly()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0 },
|
||||||
|
new HitCircle { StartTime = 1_200 }
|
||||||
|
},
|
||||||
|
Breaks = new List<BreakPeriod>
|
||||||
|
{
|
||||||
|
new BreakPeriod(100, 751)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||||
|
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateEarlyStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBreakEndsLate()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0 },
|
||||||
|
new HitCircle { StartTime = 1_298 }
|
||||||
|
},
|
||||||
|
Breaks = new List<BreakPeriod>
|
||||||
|
{
|
||||||
|
new BreakPeriod(200, 850)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||||
|
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBreakAfterLastObjectStartsEarly()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0 },
|
||||||
|
new HitCircle { StartTime = 1200 }
|
||||||
|
},
|
||||||
|
Breaks = new List<BreakPeriod>
|
||||||
|
{
|
||||||
|
new BreakPeriod(1398, 2300)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||||
|
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateEarlyStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBreakBeforeFirstObjectEndsLate()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 1100 },
|
||||||
|
new HitCircle { StartTime = 1500 }
|
||||||
|
},
|
||||||
|
Breaks = new List<BreakPeriod>
|
||||||
|
{
|
||||||
|
new BreakPeriod(0, 652)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||||
|
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBreakMultipleObjectsEarly()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0 },
|
||||||
|
new HitCircle { StartTime = 1_297 },
|
||||||
|
new HitCircle { StartTime = 1_298 }
|
||||||
|
},
|
||||||
|
Breaks = new List<BreakPeriod>
|
||||||
|
{
|
||||||
|
new BreakPeriod(200, 850)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||||
|
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBreaksCorrect()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0 },
|
||||||
|
new HitCircle { StartTime = 1_300 }
|
||||||
|
},
|
||||||
|
Breaks = new List<BreakPeriod>
|
||||||
|
{
|
||||||
|
new BreakPeriod(200, 850)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||||
|
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Is.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
85
osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs
Normal file
85
osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.Timing;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Edit.Checks;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Editing.Checks
|
||||||
|
{
|
||||||
|
public class CheckDrainLengthTest
|
||||||
|
{
|
||||||
|
private CheckDrainLength check = null!;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
check = new CheckDrainLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDrainTimeShort()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0 },
|
||||||
|
new HitCircle { StartTime = 29_999 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||||
|
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckDrainLength.IssueTemplateTooShort);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDrainTimeBreak()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0 },
|
||||||
|
new HitCircle { StartTime = 40_000 }
|
||||||
|
},
|
||||||
|
Breaks = new List<BreakPeriod>
|
||||||
|
{
|
||||||
|
new BreakPeriod(10_000, 21_000)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||||
|
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(issues.Single().Template is CheckDrainLength.IssueTemplateTooShort);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDrainTimeCorrect()
|
||||||
|
{
|
||||||
|
var hitObjects = new List<HitObject>();
|
||||||
|
|
||||||
|
for (int i = 0; i <= 30; ++i)
|
||||||
|
hitObjects.Add(new HitCircle { StartTime = 1000 * i });
|
||||||
|
|
||||||
|
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||||
|
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Is.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
@ -80,7 +81,9 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
{
|
{
|
||||||
TestLifetimeEntry entry = null;
|
TestLifetimeEntry entry = null;
|
||||||
AddStep("Create entry", () => entry = new TestLifetimeEntry(new HitObject()) { LifetimeStart = 1 });
|
AddStep("Create entry", () => entry = new TestLifetimeEntry(new HitObject()) { LifetimeStart = 1 });
|
||||||
|
assertJudged(() => entry, false);
|
||||||
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
|
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
|
||||||
|
assertJudged(() => entry, false);
|
||||||
AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET);
|
AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET);
|
||||||
|
|
||||||
TestDrawableHitObject dho = null;
|
TestDrawableHitObject dho = null;
|
||||||
@ -91,6 +94,7 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
});
|
});
|
||||||
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
|
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
|
||||||
AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY);
|
AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY);
|
||||||
|
assertJudged(() => entry, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -138,6 +142,29 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss);
|
AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestJudgedStateThroughLifetime()
|
||||||
|
{
|
||||||
|
TestDrawableHitObject dho = null;
|
||||||
|
HitObjectLifetimeEntry lifetimeEntry = null;
|
||||||
|
|
||||||
|
AddStep("Create lifetime entry", () => lifetimeEntry = new HitObjectLifetimeEntry(new HitObject { StartTime = Time.Current }));
|
||||||
|
|
||||||
|
assertJudged(() => lifetimeEntry, false);
|
||||||
|
|
||||||
|
AddStep("Create DHO and apply entry", () =>
|
||||||
|
{
|
||||||
|
Child = dho = new TestDrawableHitObject();
|
||||||
|
dho.Apply(lifetimeEntry);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertJudged(() => lifetimeEntry, false);
|
||||||
|
|
||||||
|
AddStep("Apply result", () => dho.MissForcefully());
|
||||||
|
|
||||||
|
assertJudged(() => lifetimeEntry, true);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestResultSetBeforeLoadComplete()
|
public void TestResultSetBeforeLoadComplete()
|
||||||
{
|
{
|
||||||
@ -154,15 +181,20 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
assertJudged(() => lifetimeEntry, true);
|
||||||
AddStep("Create DHO and apply entry", () =>
|
AddStep("Create DHO and apply entry", () =>
|
||||||
{
|
{
|
||||||
dho = new TestDrawableHitObject();
|
dho = new TestDrawableHitObject();
|
||||||
dho.Apply(lifetimeEntry);
|
dho.Apply(lifetimeEntry);
|
||||||
Child = dho;
|
Child = dho;
|
||||||
});
|
});
|
||||||
|
assertJudged(() => lifetimeEntry, true);
|
||||||
AddAssert("DHO state is correct", () => dho.State.Value, () => Is.EqualTo(ArmedState.Hit));
|
AddAssert("DHO state is correct", () => dho.State.Value, () => Is.EqualTo(ArmedState.Hit));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void assertJudged(Func<HitObjectLifetimeEntry> entry, bool val) =>
|
||||||
|
AddAssert(val ? "Is judged" : "Not judged", () => entry().Judged, () => Is.EqualTo(val));
|
||||||
|
|
||||||
private partial class TestDrawableHitObject : DrawableHitObject
|
private partial class TestDrawableHitObject : DrawableHitObject
|
||||||
{
|
{
|
||||||
public const double INITIAL_LIFETIME_OFFSET = 100;
|
public const double INITIAL_LIFETIME_OFFSET = 100;
|
||||||
|
@ -87,10 +87,10 @@ namespace osu.Game.Tests.Models
|
|||||||
var mock = new Mock<IScoreInfo>();
|
var mock = new Mock<IScoreInfo>();
|
||||||
|
|
||||||
mock.Setup(m => m.User).Returns(new APIUser { Username = "user" }); // TODO: temporary.
|
mock.Setup(m => m.User).Returns(new APIUser { Username = "user" }); // TODO: temporary.
|
||||||
mock.Setup(m => m.Beatmap.Metadata.Artist).Returns("artist");
|
mock.Setup(m => m.Beatmap!.Metadata.Artist).Returns("artist");
|
||||||
mock.Setup(m => m.Beatmap.Metadata.Title).Returns("title");
|
mock.Setup(m => m.Beatmap!.Metadata.Title).Returns("title");
|
||||||
mock.Setup(m => m.Beatmap.Metadata.Author.Username).Returns("author");
|
mock.Setup(m => m.Beatmap!.Metadata.Author.Username).Returns("author");
|
||||||
mock.Setup(m => m.Beatmap.DifficultyName).Returns("difficulty");
|
mock.Setup(m => m.Beatmap!.DifficultyName).Returns("difficulty");
|
||||||
|
|
||||||
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("user playing artist - title (author) [difficulty]"));
|
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("user playing artist - title (author) [difficulty]"));
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Artist = "The Artist",
|
Artist = "The Artist",
|
||||||
ArtistUnicode = "check unicode too",
|
ArtistUnicode = "check unicode too",
|
||||||
Title = "Title goes here",
|
Title = "Title goes here",
|
||||||
TitleUnicode = "Title goes here",
|
TitleUnicode = "TitleUnicode goes here",
|
||||||
Author = { Username = "The Author" },
|
Author = { Username = "The Author" },
|
||||||
Source = "unit tests",
|
Source = "unit tests",
|
||||||
Tags = "look for tags too",
|
Tags = "look for tags too",
|
||||||
@ -159,6 +159,34 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCase("\"artist\"", false)]
|
||||||
|
[TestCase("\"arti\"", true)]
|
||||||
|
[TestCase("\"artist title author\"", true)]
|
||||||
|
[TestCase("\"artist\" \"title\" \"author\"", false)]
|
||||||
|
[TestCase("\"an artist\"", true)]
|
||||||
|
[TestCase("\"tags too\"", false)]
|
||||||
|
[TestCase("\"tags to\"", true)]
|
||||||
|
[TestCase("\"version\"", false)]
|
||||||
|
[TestCase("\"an auteur\"", true)]
|
||||||
|
[TestCase("\"Artist\"!", true)]
|
||||||
|
[TestCase("\"The Artist\"!", false)]
|
||||||
|
[TestCase("\"the artist\"!", false)]
|
||||||
|
[TestCase("\"\\\"", true)] // nasty case, covers properly escaping user input in underlying regex.
|
||||||
|
public void TestCriteriaMatchingExactTerms(string terms, bool filtered)
|
||||||
|
{
|
||||||
|
var exampleBeatmapInfo = getExampleBeatmap();
|
||||||
|
var criteria = new FilterCriteria
|
||||||
|
{
|
||||||
|
Ruleset = new RulesetInfo { OnlineID = 6 },
|
||||||
|
AllowConvertedBeatmaps = true,
|
||||||
|
SearchText = terms
|
||||||
|
};
|
||||||
|
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||||
|
carouselItem.Filter(criteria);
|
||||||
|
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCase("", false)]
|
[TestCase("", false)]
|
||||||
[TestCase("The", false)]
|
[TestCase("The", false)]
|
||||||
@ -179,6 +207,27 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCase("", false)]
|
||||||
|
[TestCase("Goes", false)]
|
||||||
|
[TestCase("GOES", false)]
|
||||||
|
[TestCase("goes", false)]
|
||||||
|
[TestCase("title goes", false)]
|
||||||
|
[TestCase("title goes AND then something else", true)]
|
||||||
|
[TestCase("titleunicode", false)]
|
||||||
|
[TestCase("unknown", true)]
|
||||||
|
public void TestCriteriaMatchingTitle(string titleName, bool filtered)
|
||||||
|
{
|
||||||
|
var exampleBeatmapInfo = getExampleBeatmap();
|
||||||
|
var criteria = new FilterCriteria
|
||||||
|
{
|
||||||
|
Title = new FilterCriteria.OptionalTextFilter { SearchTerm = titleName }
|
||||||
|
};
|
||||||
|
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||||
|
carouselItem.Filter(criteria);
|
||||||
|
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCase("", false)]
|
[TestCase("", false)]
|
||||||
[TestCase("The", false)]
|
[TestCase("The", false)]
|
||||||
@ -188,6 +237,9 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
[TestCase("the artist AND then something else", true)]
|
[TestCase("the artist AND then something else", true)]
|
||||||
[TestCase("unicode too", false)]
|
[TestCase("unicode too", false)]
|
||||||
[TestCase("unknown", true)]
|
[TestCase("unknown", true)]
|
||||||
|
[TestCase("\"Artist\"!", true)]
|
||||||
|
[TestCase("\"The Artist\"!", false)]
|
||||||
|
[TestCase("\"the artist\"!", false)]
|
||||||
public void TestCriteriaMatchingArtist(string artistName, bool filtered)
|
public void TestCriteriaMatchingArtist(string artistName, bool filtered)
|
||||||
{
|
{
|
||||||
var exampleBeatmapInfo = getExampleBeatmap();
|
var exampleBeatmapInfo = getExampleBeatmap();
|
||||||
|
@ -23,6 +23,63 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Assert.AreEqual(4, filterCriteria.SearchTerms.Length);
|
Assert.AreEqual(4, filterCriteria.SearchTerms.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestApplyQueriesBareWordsWithExactMatch()
|
||||||
|
{
|
||||||
|
const string query = "looking for \"a beatmap\"! like \"this\"";
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual("looking for \"a beatmap\"! like \"this\"", filterCriteria.SearchText);
|
||||||
|
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
|
||||||
|
|
||||||
|
Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("a beatmap"));
|
||||||
|
Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase));
|
||||||
|
|
||||||
|
Assert.That(filterCriteria.SearchTerms[1].SearchTerm, Is.EqualTo("this"));
|
||||||
|
Assert.That(filterCriteria.SearchTerms[1].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
|
||||||
|
|
||||||
|
Assert.That(filterCriteria.SearchTerms[2].SearchTerm, Is.EqualTo("looking"));
|
||||||
|
Assert.That(filterCriteria.SearchTerms[2].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
|
||||||
|
|
||||||
|
Assert.That(filterCriteria.SearchTerms[3].SearchTerm, Is.EqualTo("for"));
|
||||||
|
Assert.That(filterCriteria.SearchTerms[3].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
|
||||||
|
|
||||||
|
Assert.That(filterCriteria.SearchTerms[4].SearchTerm, Is.EqualTo("like"));
|
||||||
|
Assert.That(filterCriteria.SearchTerms[4].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestApplyFullPhraseQueryWithExclamationPointInTerm()
|
||||||
|
{
|
||||||
|
const string query = "looking for \"circles!\"!";
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual("looking for \"circles!\"!", filterCriteria.SearchText);
|
||||||
|
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
|
||||||
|
|
||||||
|
Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("circles!"));
|
||||||
|
Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase));
|
||||||
|
|
||||||
|
Assert.That(filterCriteria.SearchTerms[1].SearchTerm, Is.EqualTo("looking"));
|
||||||
|
Assert.That(filterCriteria.SearchTerms[1].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
|
||||||
|
|
||||||
|
Assert.That(filterCriteria.SearchTerms[2].SearchTerm, Is.EqualTo("for"));
|
||||||
|
Assert.That(filterCriteria.SearchTerms[2].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestApplyBrokenFullPhraseQuery()
|
||||||
|
{
|
||||||
|
const string query = "\"!";
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual("\"!", filterCriteria.SearchText);
|
||||||
|
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
|
||||||
|
|
||||||
|
Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("!"));
|
||||||
|
Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The following tests have been written a bit strangely (they don't check exact
|
* The following tests have been written a bit strangely (they don't check exact
|
||||||
* bound equality with what the filter says).
|
* bound equality with what the filter says).
|
||||||
@ -226,6 +283,18 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Assert.AreEqual("my_fav", filterCriteria.Creator.SearchTerm);
|
Assert.AreEqual("my_fav", filterCriteria.Creator.SearchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestApplyTitleQueries()
|
||||||
|
{
|
||||||
|
const string query = "find me songs with title=\"a certain title\" please";
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual("find me songs with please", filterCriteria.SearchText.Trim());
|
||||||
|
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
|
||||||
|
Assert.AreEqual("a certain title", filterCriteria.Title.SearchTerm);
|
||||||
|
Assert.That(filterCriteria.Title.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestApplyArtistQueries()
|
public void TestApplyArtistQueries()
|
||||||
{
|
{
|
||||||
@ -235,6 +304,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim());
|
Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim());
|
||||||
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
|
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
|
||||||
Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm);
|
Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm);
|
||||||
|
Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -246,6 +316,19 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim());
|
Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim());
|
||||||
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
|
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
|
||||||
Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm);
|
Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm);
|
||||||
|
Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestApplyArtistQueriesWithSpacesFullPhrase()
|
||||||
|
{
|
||||||
|
const string query = "artist=\"The Only One\"!";
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.That(filterCriteria.SearchText.Trim(), Is.Empty);
|
||||||
|
Assert.AreEqual(0, filterCriteria.SearchTerms.Length);
|
||||||
|
Assert.AreEqual("The Only One", filterCriteria.Artist.SearchTerm);
|
||||||
|
Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user