1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 23:27:25 +08:00

Merge branch 'ppy:master' into mania-difficulty-refactor

This commit is contained in:
molneya 2022-07-01 11:04:55 +00:00 committed by GitHub
commit 11c7756670
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
436 changed files with 1782 additions and 1615 deletions

View File

@ -16,3 +16,6 @@ M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead.
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks. M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks. P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks.
M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever.
M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.

View File

@ -1,7 +1,7 @@
<!-- Contains required properties for osu!framework projects. --> <!-- Contains required properties for osu!framework projects. -->
<Project> <Project>
<PropertyGroup Label="C#"> <PropertyGroup Label="C#">
<LangVersion>8.0</LangVersion> <LangVersion>9.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@ -8,20 +8,20 @@ GEM
artifactory (3.0.15) artifactory (3.0.15)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.570.0) aws-partitions (1.601.0)
aws-sdk-core (3.130.0) aws-sdk-core (3.131.2)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0) aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.55.0) aws-sdk-kms (1.57.0)
aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.113.0) aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4) aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0) aws-sigv4 (1.5.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4) babosa (1.0.4)
claide (1.1.0) claide (1.1.0)
@ -36,7 +36,7 @@ GEM
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6) dotenv (2.7.6)
emoji_regex (3.2.3) emoji_regex (3.2.3)
excon (0.92.1) excon (0.92.3)
faraday (1.10.0) faraday (1.10.0)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
@ -56,8 +56,8 @@ GEM
faraday-em_synchrony (1.0.0) faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0) faraday-excon (1.1.0)
faraday-httpclient (1.0.1) faraday-httpclient (1.0.1)
faraday-multipart (1.0.3) faraday-multipart (1.0.4)
multipart-post (>= 1.2, < 3) multipart-post (~> 2)
faraday-net_http (1.0.1) faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0) faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0) faraday-patron (1.0.0)
@ -66,7 +66,7 @@ GEM
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.2.6) fastimage (2.2.6)
fastlane (2.205.1) fastlane (2.206.2)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
@ -110,9 +110,9 @@ GEM
souyuz (= 0.11.1) souyuz (= 0.11.1)
fastlane-plugin-xamarin (0.6.3) fastlane-plugin-xamarin (0.6.3)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.16.0) google-apis-androidpublisher_v3 (0.23.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.6, < 2.a)
google-apis-core (0.4.2) google-apis-core (0.6.0)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
@ -121,19 +121,19 @@ GEM
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml rexml
webrick webrick
google-apis-iamcredentials_v1 (0.10.0) google-apis-iamcredentials_v1 (0.12.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.6, < 2.a)
google-apis-playcustomapp_v1 (0.7.0) google-apis-playcustomapp_v1 (0.9.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.6, < 2.a)
google-apis-storage_v1 (0.11.0) google-apis-storage_v1 (0.16.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.6, < 2.a)
google-cloud-core (1.6.0) google-cloud-core (1.6.0)
google-cloud-env (~> 1.0) google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0) google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0) faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0) google-cloud-errors (1.2.0)
google-cloud-storage (1.36.1) google-cloud-storage (1.36.2)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.1)
@ -141,7 +141,7 @@ GEM
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.1.2) googleauth (1.2.0)
faraday (>= 0.17.3, < 3.a) faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
@ -149,12 +149,12 @@ GEM
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
highline (2.0.3) highline (2.0.3)
http-cookie (1.0.4) http-cookie (1.0.5)
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.8.3) httpclient (2.8.3)
jmespath (1.6.1) jmespath (1.6.1)
json (2.6.1) json (2.6.2)
jwt (2.3.0) jwt (2.4.1)
memoist (0.16.2) memoist (0.16.2)
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.1.2) mini_mime (1.1.2)
@ -169,10 +169,10 @@ GEM
optparse (0.1.1) optparse (0.1.1)
os (1.1.4) os (1.1.4)
plist (3.6.0) plist (3.6.0)
public_suffix (4.0.6) public_suffix (4.0.7)
racc (1.6.0) racc (1.6.0)
rake (13.0.6) rake (13.0.6)
representable (3.1.1) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
@ -182,9 +182,9 @@ GEM
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
security (0.1.3) security (0.1.3)
signet (0.16.1) signet (0.17.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.0) faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
multi_json (~> 1.10) multi_json (~> 1.10)
simctl (1.6.8) simctl (1.6.8)
@ -205,11 +205,11 @@ GEM
uber (0.1.0) uber (0.1.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.8.1) unf_ext (0.0.8.2)
unicode-display_width (1.8.0) unicode-display_width (1.8.0)
webrick (1.7.0) webrick (1.7.0)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.21.0) xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.618.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.628.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.621.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.629.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -57,7 +57,7 @@ namespace osu.Desktop
client.OnReady += onReady; client.OnReady += onReady;
// safety measure for now, until we performance test / improve backoff for failed connections. // safety measure for now, until we performance test / improve backoff for failed connections.
client.OnConnectionFailed += (_, __) => client.Deinitialize(); 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);

View File

@ -129,18 +129,18 @@ namespace osu.Desktop
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private static void setupSquirrel() private static void setupSquirrel()
{ {
SquirrelAwareApp.HandleEvents(onInitialInstall: (version, tools) => SquirrelAwareApp.HandleEvents(onInitialInstall: (_, tools) =>
{ {
tools.CreateShortcutForThisExe(); tools.CreateShortcutForThisExe();
tools.CreateUninstallerRegistryEntry(); tools.CreateUninstallerRegistryEntry();
}, onAppUpdate: (version, tools) => }, onAppUpdate: (_, tools) =>
{ {
tools.CreateUninstallerRegistryEntry(); tools.CreateUninstallerRegistryEntry();
}, onAppUninstall: (version, tools) => }, onAppUninstall: (_, tools) =>
{ {
tools.RemoveShortcutForThisExe(); tools.RemoveShortcutForThisExe();
tools.RemoveUninstallerRegistryEntry(); tools.RemoveUninstallerRegistryEntry();
}, onEveryRun: (version, tools, firstRun) => }, onEveryRun: (_, _, _) =>
{ {
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently // While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
// causes the right-click context menu to function incorrectly. // causes the right-click context menu to function incorrectly.

View File

@ -31,7 +31,7 @@ namespace osu.Game.Benchmarks
realm = new RealmAccess(storage, OsuGameBase.CLIENT_DATABASE_FILENAME); realm = new RealmAccess(storage, OsuGameBase.CLIENT_DATABASE_FILENAME);
realm.Run(r => realm.Run(_ =>
{ {
realm.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo }))); realm.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo })));
}); });
@ -76,7 +76,7 @@ namespace osu.Game.Benchmarks
} }
}); });
done.Wait(); done.Wait(60000);
} }
[Benchmark] [Benchmark]
@ -115,7 +115,7 @@ namespace osu.Game.Benchmarks
} }
}); });
done.Wait(); done.Wait(60000);
} }
[Benchmark] [Benchmark]

View File

@ -24,21 +24,24 @@ namespace osu.Game.Rulesets.Catch.Tests
new object[] { LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) } }, new object[] { LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) } },
new object[] { LegacyMods.Relax, new[] { typeof(CatchModRelax) } }, new object[] { LegacyMods.Relax, new[] { typeof(CatchModRelax) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } }, new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } },
new object[] { LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) } },
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.Perfect, new[] { typeof(CatchModPerfect) } },
new object[] { LegacyMods.Cinema, new[] { typeof(CatchModCinema) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } } new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }
}; };
[TestCaseSource(nameof(catch_mod_mapping))]
[TestCase(LegacyMods.Cinema, new[] { typeof(CatchModCinema) })]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })]
[TestCase(LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })]
[TestCase(LegacyMods.Perfect, new[] { typeof(CatchModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })]
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(catch_mod_mapping))] [TestCaseSource(nameof(catch_mod_mapping))]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })] [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })]
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(catch_mod_mapping))]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new CatchRuleset(); protected override Ruleset CreateRuleset() => new CatchRuleset();

View File

@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Tests
new JuiceStreamPathVertex(20, -5) new JuiceStreamPathVertex(20, -5)
})); }));
removeCount = path.RemoveVertices((_, i) => true); removeCount = path.RemoveVertices((_, _) => true);
Assert.That(removeCount, Is.EqualTo(1)); Assert.That(removeCount, Is.EqualTo(1));
Assert.That(path.Vertices, Is.EqualTo(new[] Assert.That(path.Vertices, Is.EqualTo(new[]
{ {

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f)); AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f));
AddStep("update target", () => Player.ChildrenOfType<SkinnableTargetContainer>().ForEach(LegacySkin.UpdateDrawableTarget)); AddStep("update target", () => Player.ChildrenOfType<SkinnableTargetContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
AddStep("exit player", () => Player.Exit()); AddStep("exit player", () => Player.Exit());
CreateTest(null); CreateTest();
} }
AddAssert("legacy HUD combo counter hidden", () => AddAssert("legacy HUD combo counter hidden", () =>

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Tests
hyperDashCount = 0; hyperDashCount = 0;
// this needs to be done within the frame stable context due to how quickly hyperdash state changes occur. // this needs to be done within the frame stable context due to how quickly hyperdash state changes occur.
Player.DrawableRuleset.FrameStableComponents.OnUpdate += d => Player.DrawableRuleset.FrameStableComponents.OnUpdate += _ =>
{ {
var catcher = Player.ChildrenOfType<Catcher>().FirstOrDefault(); var catcher = Player.ChildrenOfType<Catcher>().FirstOrDefault();

View File

@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Catch
protected override string RulesetPrefix => "catch"; // todo: use CatchRuleset.SHORT_NAME; protected override string RulesetPrefix => "catch"; // todo: use CatchRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLower(); protected override string ComponentName => Component.ToString().ToLowerInvariant();
} }
} }

View File

@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Catch.Difficulty namespace osu.Game.Rulesets.Catch.Difficulty
@ -31,9 +30,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
} }
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values) public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
{ {
base.FromDatabaseAttributes(values); base.FromDatabaseAttributes(values, onlineInfo);
StarRating = values[ATTRIB_ID_AIM]; StarRating = values[ATTRIB_ID_AIM];
ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; ApproachRate = values[ATTRIB_ID_APPROACH_RATE];

View File

@ -135,15 +135,15 @@ namespace osu.Game.Rulesets.Catch.Edit
{ {
switch (BlueprintContainer.CurrentTool) switch (BlueprintContainer.CurrentTool)
{ {
case SelectTool _: case SelectTool:
if (EditorBeatmap.SelectedHitObjects.Count == 0) if (EditorBeatmap.SelectedHitObjects.Count == 0)
return null; return null;
double minTime = EditorBeatmap.SelectedHitObjects.Min(hitObject => hitObject.StartTime); double minTime = EditorBeatmap.SelectedHitObjects.Min(hitObject => hitObject.StartTime);
return getLastSnappableHitObject(minTime); return getLastSnappableHitObject(minTime);
case FruitCompositionTool _: case FruitCompositionTool:
case JuiceStreamCompositionTool _: case JuiceStreamCompositionTool:
if (!CursorInPlacementArea) if (!CursorInPlacementArea)
return null; return null;

View File

@ -42,10 +42,10 @@ namespace osu.Game.Rulesets.Catch.Edit
case Droplet droplet: case Droplet droplet:
return droplet is TinyDroplet ? PositionRange.EMPTY : new PositionRange(droplet.OriginalX); return droplet is TinyDroplet ? PositionRange.EMPTY : new PositionRange(droplet.OriginalX);
case JuiceStream _: case JuiceStream:
return GetPositionRange(hitObject.NestedHitObjects); return GetPositionRange(hitObject.NestedHitObjects);
case BananaShower _: case BananaShower:
// A banana shower occupies the whole screen width. // A banana shower occupies the whole screen width.
return new PositionRange(0, CatchPlayfield.WIDTH); return new PositionRange(0, CatchPlayfield.WIDTH);

View File

@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Catch.Edit
{ {
switch (hitObject) switch (hitObject)
{ {
case BananaShower _: case BananaShower:
return false; return false;
case JuiceStream juiceStream: case JuiceStream juiceStream:

View File

@ -389,13 +389,13 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
switch (source) switch (source)
{ {
case Fruit _: case Fruit:
return caughtFruitPool.Get(); return caughtFruitPool.Get();
case Banana _: case Banana:
return caughtBananaPool.Get(); return caughtBananaPool.Get();
case Droplet _: case Droplet:
return caughtDropletPool.Get(); return caughtDropletPool.Get();
default: default:

View File

@ -23,10 +23,8 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { LegacyMods.SuddenDeath, new[] { typeof(ManiaModSuddenDeath) } }, new object[] { LegacyMods.SuddenDeath, new[] { typeof(ManiaModSuddenDeath) } },
new object[] { LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) } }, new object[] { LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(ManiaModHalfTime) } }, new object[] { LegacyMods.HalfTime, new[] { typeof(ManiaModHalfTime) } },
new object[] { LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(ManiaModFlashlight) } }, new object[] { LegacyMods.Flashlight, new[] { typeof(ManiaModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(ManiaModAutoplay) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(ManiaModAutoplay) } },
new object[] { LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) } },
new object[] { LegacyMods.Key4, new[] { typeof(ManiaModKey4) } }, new object[] { LegacyMods.Key4, new[] { typeof(ManiaModKey4) } },
new object[] { LegacyMods.Key5, new[] { typeof(ManiaModKey5) } }, new object[] { LegacyMods.Key5, new[] { typeof(ManiaModKey5) } },
new object[] { LegacyMods.Key6, new[] { typeof(ManiaModKey6) } }, new object[] { LegacyMods.Key6, new[] { typeof(ManiaModKey6) } },
@ -34,7 +32,6 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { LegacyMods.Key8, new[] { typeof(ManiaModKey8) } }, new object[] { LegacyMods.Key8, new[] { typeof(ManiaModKey8) } },
new object[] { LegacyMods.FadeIn, new[] { typeof(ManiaModFadeIn) } }, new object[] { LegacyMods.FadeIn, new[] { typeof(ManiaModFadeIn) } },
new object[] { LegacyMods.Random, new[] { typeof(ManiaModRandom) } }, new object[] { LegacyMods.Random, new[] { typeof(ManiaModRandom) } },
new object[] { LegacyMods.Cinema, new[] { typeof(ManiaModCinema) } },
new object[] { LegacyMods.Key9, new[] { typeof(ManiaModKey9) } }, new object[] { LegacyMods.Key9, new[] { typeof(ManiaModKey9) } },
new object[] { LegacyMods.KeyCoop, new[] { typeof(ManiaModDualStages) } }, new object[] { LegacyMods.KeyCoop, new[] { typeof(ManiaModDualStages) } },
new object[] { LegacyMods.Key1, new[] { typeof(ManiaModKey1) } }, new object[] { LegacyMods.Key1, new[] { typeof(ManiaModKey1) } },
@ -45,12 +42,18 @@ namespace osu.Game.Rulesets.Mania.Tests
}; };
[TestCaseSource(nameof(mania_mod_mapping))] [TestCaseSource(nameof(mania_mod_mapping))]
[TestCase(LegacyMods.Cinema, new[] { typeof(ManiaModCinema) })]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })] [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })]
[TestCase(LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })]
[TestCase(LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })]
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(mania_mod_mapping))] [TestCaseSource(nameof(mania_mod_mapping))]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new ManiaRuleset(); protected override Ruleset CreateRuleset() => new ManiaRuleset();

View File

@ -173,7 +173,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
switch (original) switch (original)
{ {
case IHasDistance _: case IHasDistance:
{ {
var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap); var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
conversion = generator; conversion = generator;

View File

@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Mania.Difficulty namespace osu.Game.Rulesets.Mania.Difficulty
@ -20,12 +19,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
[JsonProperty("great_hit_window")] [JsonProperty("great_hit_window")]
public double GreatHitWindow { get; set; } public double GreatHitWindow { get; set; }
/// <summary>
/// The score multiplier applied via score-reducing mods.
/// </summary>
[JsonProperty("score_multiplier")]
public double ScoreMultiplier { get; set; }
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
{ {
foreach (var v in base.ToDatabaseAttributes()) foreach (var v in base.ToDatabaseAttributes())
@ -34,17 +27,15 @@ namespace osu.Game.Rulesets.Mania.Difficulty
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); 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);
yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier);
} }
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values) public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
{ {
base.FromDatabaseAttributes(values); base.FromDatabaseAttributes(values, onlineInfo);
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; 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];
ScoreMultiplier = values[ATTRIB_ID_SCORE_MULTIPLIER];
} }
} }
} }

View File

@ -53,7 +53,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// 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),
ScoreMultiplier = getScoreMultiplier(mods),
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject) MaxCombo = beatmap.HitObjects.Sum(maxComboForObject)
}; };
} }
@ -147,32 +146,5 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return value; return value;
} }
} }
private double getScoreMultiplier(Mod[] mods)
{
double scoreMultiplier = 1;
foreach (var m in mods)
{
switch (m)
{
case ManiaModNoFail _:
case ManiaModEasy _:
case ManiaModHalfTime _:
scoreMultiplier *= 0.5;
break;
}
}
var maniaBeatmap = (ManiaBeatmap)Beatmap;
int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
if (diff > 0)
scoreMultiplier *= 0.9;
else if (diff < 0)
scoreMultiplier *= 0.9 + 0.04 * diff;
return scoreMultiplier;
}
} }
} }

View File

@ -14,19 +14,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
[JsonProperty("difficulty")] [JsonProperty("difficulty")]
public double Difficulty { get; set; } public double Difficulty { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty("scaled_score")]
public double ScaledScore { get; set; }
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay() public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
{ {
foreach (var attribute in base.GetAttributesForDisplay()) foreach (var attribute in base.GetAttributesForDisplay())
yield return attribute; yield return attribute;
yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty); yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty);
yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy);
} }
} }
} }

View File

@ -15,15 +15,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{ {
public class ManiaPerformanceCalculator : PerformanceCalculator public class ManiaPerformanceCalculator : PerformanceCalculator
{ {
// Score after being scaled by non-difficulty-increasing mods
private double scaledScore;
private int countPerfect; private int countPerfect;
private int countGreat; private int countGreat;
private int countGood; private int countGood;
private int countOk; private int countOk;
private int countMeh; private int countMeh;
private int countMiss; private int countMiss;
private double scoreAccuracy;
public ManiaPerformanceCalculator() public ManiaPerformanceCalculator()
: base(new ManiaRuleset()) : base(new ManiaRuleset())
@ -34,82 +32,47 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{ {
var maniaAttributes = (ManiaDifficultyAttributes)attributes; var maniaAttributes = (ManiaDifficultyAttributes)attributes;
scaledScore = score.TotalScore;
countPerfect = score.Statistics.GetValueOrDefault(HitResult.Perfect); countPerfect = score.Statistics.GetValueOrDefault(HitResult.Perfect);
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great); countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
countGood = score.Statistics.GetValueOrDefault(HitResult.Good); countGood = score.Statistics.GetValueOrDefault(HitResult.Good);
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
scoreAccuracy = customAccuracy;
if (maniaAttributes.ScoreMultiplier > 0)
{
// Scale score up, so it's comparable to other keymods
scaledScore *= 1.0 / maniaAttributes.ScoreMultiplier;
}
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
// The specific number has no intrinsic meaning and can be adjusted as needed. // The specific number has no intrinsic meaning and can be adjusted as needed.
double multiplier = 0.8; double multiplier = 8.0;
if (score.Mods.Any(m => m is ModNoFail)) if (score.Mods.Any(m => m is ModNoFail))
multiplier *= 0.9; multiplier *= 0.75;
if (score.Mods.Any(m => m is ModEasy)) if (score.Mods.Any(m => m is ModEasy))
multiplier *= 0.5; multiplier *= 0.5;
double difficultyValue = computeDifficultyValue(maniaAttributes); double difficultyValue = computeDifficultyValue(maniaAttributes);
double accValue = computeAccuracyValue(difficultyValue, maniaAttributes); double totalValue = difficultyValue * multiplier;
double totalValue =
Math.Pow(
Math.Pow(difficultyValue, 1.1) +
Math.Pow(accValue, 1.1), 1.0 / 1.1
) * multiplier;
return new ManiaPerformanceAttributes return new ManiaPerformanceAttributes
{ {
Difficulty = difficultyValue, Difficulty = difficultyValue,
Accuracy = accValue,
ScaledScore = scaledScore,
Total = totalValue Total = totalValue
}; };
} }
private double computeDifficultyValue(ManiaDifficultyAttributes attributes) private double computeDifficultyValue(ManiaDifficultyAttributes attributes)
{ {
double difficultyValue = Math.Pow(5 * Math.Max(1, attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0; double difficultyValue = Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve
* Math.Max(0, 5 * scoreAccuracy - 4) // From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy
difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); * (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes
if (scaledScore <= 500000)
difficultyValue = 0;
else if (scaledScore <= 600000)
difficultyValue *= (scaledScore - 500000) / 100000 * 0.3;
else if (scaledScore <= 700000)
difficultyValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25;
else if (scaledScore <= 800000)
difficultyValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20;
else if (scaledScore <= 900000)
difficultyValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15;
else
difficultyValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1;
return difficultyValue; return difficultyValue;
} }
private double computeAccuracyValue(double difficultyValue, ManiaDifficultyAttributes attributes)
{
if (attributes.GreatHitWindow <= 0)
return 0;
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accuracyValue = Math.Max(0.0, 0.2 - (attributes.GreatHitWindow - 34) * 0.006667)
* difficultyValue
* Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);
return accuracyValue;
}
private double totalHits => countPerfect + countOk + countGreat + countGood + countMeh + countMiss; private double totalHits => countPerfect + countOk + countGreat + countGood + countMeh + countMiss;
/// <summary>
/// Accuracy used to weight judgements independently from the score's actual accuracy.
/// </summary>
private double customAccuracy => (countPerfect * 320 + countGreat * 300 + countGood * 200 + countOk * 100 + countMeh * 50) / (totalHits * 320);
} }
} }

View File

@ -146,56 +146,56 @@ namespace osu.Game.Rulesets.Mania
{ {
switch (mod) switch (mod)
{ {
case ManiaModKey1 _: case ManiaModKey1:
value |= LegacyMods.Key1; value |= LegacyMods.Key1;
break; break;
case ManiaModKey2 _: case ManiaModKey2:
value |= LegacyMods.Key2; value |= LegacyMods.Key2;
break; break;
case ManiaModKey3 _: case ManiaModKey3:
value |= LegacyMods.Key3; value |= LegacyMods.Key3;
break; break;
case ManiaModKey4 _: case ManiaModKey4:
value |= LegacyMods.Key4; value |= LegacyMods.Key4;
break; break;
case ManiaModKey5 _: case ManiaModKey5:
value |= LegacyMods.Key5; value |= LegacyMods.Key5;
break; break;
case ManiaModKey6 _: case ManiaModKey6:
value |= LegacyMods.Key6; value |= LegacyMods.Key6;
break; break;
case ManiaModKey7 _: case ManiaModKey7:
value |= LegacyMods.Key7; value |= LegacyMods.Key7;
break; break;
case ManiaModKey8 _: case ManiaModKey8:
value |= LegacyMods.Key8; value |= LegacyMods.Key8;
break; break;
case ManiaModKey9 _: case ManiaModKey9:
value |= LegacyMods.Key9; value |= LegacyMods.Key9;
break; break;
case ManiaModDualStages _: case ManiaModDualStages:
value |= LegacyMods.KeyCoop; value |= LegacyMods.KeyCoop;
break; break;
case ManiaModFadeIn _: case ManiaModFadeIn:
value |= LegacyMods.FadeIn; value |= LegacyMods.FadeIn;
value &= ~LegacyMods.Hidden; // this is toggled on in the base call due to inheritance, but we don't want that. value &= ~LegacyMods.Hidden; // this is toggled on in the base call due to inheritance, but we don't want that.
break; break;
case ManiaModMirror _: case ManiaModMirror:
value |= LegacyMods.Mirror; value |= LegacyMods.Mirror;
break; break;
case ManiaModRandom _: case ManiaModRandom:
value |= LegacyMods.Random; value |= LegacyMods.Random;
break; break;
} }

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania
protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME; protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLower(); protected override string ComponentName => Component.ToString().ToLowerInvariant();
} }
public enum ManiaSkinComponents public enum ManiaSkinComponents

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Mods
var rng = new Random((int)Seed.Value); var rng = new Random((int)Seed.Value);
int availableColumns = ((ManiaBeatmap)beatmap).TotalColumns; int availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => rng.Next()).ToList(); var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(_ => rng.Next()).ToList();
beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = shuffledColumns[h.Column]); beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = shuffledColumns[h.Column]);
} }

View File

@ -57,11 +57,11 @@ namespace osu.Game.Rulesets.Mania.Replays
{ {
switch (point) switch (point)
{ {
case HitPoint _: case HitPoint:
actions.Add(columnActions[point.Column]); actions.Add(columnActions[point.Column]);
break; break;
case ReleasePoint _: case ReleasePoint:
actions.Remove(columnActions[point.Column]); actions.Remove(columnActions[point.Column]);
break; break;
} }

View File

@ -10,7 +10,6 @@ using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
@ -41,10 +40,6 @@ 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);
private bool editorComponentsReady => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
&& editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true
&& editor?.ChildrenOfType<Playfield>().FirstOrDefault()?.IsLoaded == true;
[TestCase(true)] [TestCase(true)]
[TestCase(false)] [TestCase(false)]
public void TestVelocityChangeSavesCorrectly(bool adjustVelocity) public void TestVelocityChangeSavesCorrectly(bool adjustVelocity)
@ -52,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
double? velocity = null; double? velocity = null;
AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader())); AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader()));
AddUntilStep("wait for editor load", () => editorComponentsReady); AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time)); AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time));
AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3)); AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3));
@ -91,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("exit", () => InputManager.Key(Key.Escape)); AddStep("exit", () => InputManager.Key(Key.Escape));
AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader())); AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader()));
AddUntilStep("wait for editor load", () => editorComponentsReady); AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
AddStep("seek to slider", () => editorClock.Seek(slider.StartTime)); AddStep("seek to slider", () => editorClock.Seek(slider.StartTime));
AddAssert("slider has correct velocity", () => slider.Velocity == velocity); AddAssert("slider has correct velocity", () => slider.Velocity == velocity);

View File

@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Tests
skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase(); skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase();
skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>())) skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>()))
.Returns((string componentName, WrapMode _, WrapMode __) => new Texture(1, 1) { AssetName = componentName }); .Returns((string componentName, WrapMode _, WrapMode _) => new Texture(1, 1) { AssetName = componentName });
Child = new DependencyProvidingContainer Child = new DependencyProvidingContainer
{ {

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
lastResult = null; lastResult = null;
spinner = nextSpinner; spinner = nextSpinner;
spinner.OnNewResult += (o, result) => lastResult = result; spinner.OnNewResult += (_, result) => lastResult = result;
} }
return lastResult?.Type == HitResult.Great; return lastResult?.Type == HitResult.Great;
@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
return false; return false;
spinner = nextSpinner; spinner = nextSpinner;
spinner.OnNewResult += (o, result) => results.Add(result); spinner.OnNewResult += (_, result) => results.Add(result);
results.Clear(); results.Clear();
} }

View File

@ -25,24 +25,27 @@ namespace osu.Game.Rulesets.Osu.Tests
new object[] { LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) } }, new object[] { LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) } },
new object[] { LegacyMods.Relax, new[] { typeof(OsuModRelax) } }, new object[] { LegacyMods.Relax, new[] { typeof(OsuModRelax) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(OsuModHalfTime) } }, new object[] { LegacyMods.HalfTime, new[] { typeof(OsuModHalfTime) } },
new object[] { LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(OsuModFlashlight) } }, new object[] { LegacyMods.Flashlight, new[] { typeof(OsuModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(OsuModAutoplay) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(OsuModAutoplay) } },
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.Perfect, new[] { typeof(OsuModPerfect) } },
new object[] { LegacyMods.Cinema, new[] { typeof(OsuModCinema) } },
new object[] { LegacyMods.Target, new[] { typeof(OsuModTarget) } }, new object[] { LegacyMods.Target, new[] { typeof(OsuModTarget) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } } new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }
}; };
[TestCaseSource(nameof(osu_mod_mapping))] [TestCaseSource(nameof(osu_mod_mapping))]
[TestCase(LegacyMods.Cinema, new[] { typeof(OsuModCinema) })]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })] [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })]
[TestCase(LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })]
[TestCase(LegacyMods.Perfect, new[] { typeof(OsuModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })]
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(osu_mod_mapping))] [TestCaseSource(nameof(osu_mod_mapping))]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new OsuRuleset(); protected override Ruleset CreateRuleset() => new OsuRuleset();

View File

@ -14,7 +14,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
@ -93,10 +92,10 @@ namespace osu.Game.Rulesets.Osu.Tests
}); });
AddStep("set accent white", () => dho.AccentColour.Value = Color4.White); AddStep("set accent white", () => dho.AccentColour.Value = Color4.White);
AddAssert("ball is white", () => dho.ChildrenOfType<SliderBall>().Single().AccentColour == Color4.White); AddAssert("ball is white", () => dho.ChildrenOfType<DrawableSliderBall>().Single().AccentColour == Color4.White);
AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red); AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red);
AddAssert("ball is red", () => dho.ChildrenOfType<SliderBall>().Single().AccentColour == Color4.Red); AddAssert("ball is red", () => dho.ChildrenOfType<DrawableSliderBall>().Single().AccentColour == Color4.Red);
} }
private Slider prepareObject(Slider slider) private Slider prepareObject(Slider slider)

View File

@ -0,0 +1,120 @@
// 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.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.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;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
[HeadlessTest]
public class TestSceneSliderFollowCircleInput : RateAdjustedBeatmapTestScene
{
private List<JudgementResult>? judgementResults;
private ScoreAccessibleReplayPlayer? currentPlayer;
[Test]
public void TestMaximumDistanceTrackingWithoutMovement(
[Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)]
float circleSize,
[Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)]
double velocity)
{
const double time_slider_start = 1000;
float circleRadius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (circleSize - 5) / 5) / 2;
float followCircleRadius = circleRadius * 1.2f;
performTest(new Beatmap<OsuHitObject>
{
HitObjects =
{
new Slider
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = velocity },
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(followCircleRadius, 0),
}, followCircleRadius),
},
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty
{
CircleSize = circleSize,
SliderTickRate = 1
},
Ruleset = new OsuRuleset().RulesetInfo
},
}, new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(-circleRadius + 1, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_start },
});
AddAssert("Tracking kept", assertMaxJudge);
}
private bool assertMaxJudge() => judgementResults?.Any() == true && judgementResults.All(t => t.Type == t.Judgement.MaxResult);
private void performTest(Beatmap<OsuHitObject> beatmap, List<ReplayFrame> frames)
{
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 == true);
}
private 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,
})
{
}
}
}
}

View File

@ -66,10 +66,7 @@ namespace osu.Game.Rulesets.Osu.Tests
drawableSlider = null; drawableSlider = null;
}); });
[SetUpSteps] protected override bool HasCustomSteps => true;
public override void SetUpSteps()
{
}
[TestCase(0)] [TestCase(0)]
[TestCase(1)] [TestCase(1)]
@ -77,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public void TestSnakingEnabled(int sliderIndex) public void TestSnakingEnabled(int sliderIndex)
{ {
AddStep("enable autoplay", () => autoplay = true); AddStep("enable autoplay", () => autoplay = true);
base.SetUpSteps(); CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
retrieveSlider(sliderIndex); retrieveSlider(sliderIndex);
@ -101,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public void TestSnakingDisabled(int sliderIndex) public void TestSnakingDisabled(int sliderIndex)
{ {
AddStep("have autoplay", () => autoplay = true); AddStep("have autoplay", () => autoplay = true);
base.SetUpSteps(); CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
retrieveSlider(sliderIndex); retrieveSlider(sliderIndex);
@ -121,8 +118,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
AddStep("enable autoplay", () => autoplay = true); AddStep("enable autoplay", () => autoplay = true);
setSnaking(true); setSnaking(true);
base.SetUpSteps(); CreateTest();
// repeat might have a chance to update its position depending on where in the frame its hit, // repeat might have a chance to update its position depending on where in the frame its hit,
// so some leniency is allowed here instead of checking strict equality // so some leniency is allowed here instead of checking strict equality
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame); addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame);
@ -133,15 +129,14 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
AddStep("disable autoplay", () => autoplay = false); AddStep("disable autoplay", () => autoplay = false);
setSnaking(true); setSnaking(true);
base.SetUpSteps(); CreateTest();
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased); addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased);
} }
private void retrieveSlider(int index) private void retrieveSlider(int index)
{ {
AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]); AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
addSeekStep(() => slider); addSeekStep(() => slider.StartTime);
AddUntilStep("retrieve drawable slider", () => AddUntilStep("retrieve drawable slider", () =>
(drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
} }
@ -161,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.Tests
=> addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame); => addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
private Func<double> timeAtRepeat(Func<double> startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex; private Func<double> timeAtRepeat(Func<double> startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex;
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func<Vector2>)getSliderStart : getSliderEnd; private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? getSliderStart : getSliderEnd;
private List<Vector2> getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; private List<Vector2> getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
private Vector2 getSliderStart() => getSliderCurve().First(); private Vector2 getSliderStart() => getSliderCurve().First();
@ -205,16 +200,10 @@ namespace osu.Game.Rulesets.Osu.Tests
}); });
} }
private void addSeekStep(Func<Slider> slider) private void addSeekStep(Func<double> getTime)
{ {
AddStep("seek to slider", () => Player.GameplayClockContainer.Seek(slider().StartTime)); AddStep("seek to time", () => Player.GameplayClockContainer.Seek(getTime()));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(slider().StartTime, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(getTime(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
private void addSeekStep(Func<double> time)
{
AddStep("seek to time", () => Player.GameplayClockContainer.Seek(time()));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
} }
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = createHitObjects() }; protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = createHitObjects() };

View File

@ -1,12 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -26,6 +25,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("speed_difficulty")] [JsonProperty("speed_difficulty")]
public double SpeedDifficulty { get; set; } public double SpeedDifficulty { get; set; }
/// <summary>
/// The number of clickable objects weighted by difficulty.
/// Related to <see cref="SpeedDifficulty"/>
/// </summary>
[JsonProperty("speed_note_count")]
public double SpeedNoteCount { get; set; }
/// <summary> /// <summary>
/// The difficulty corresponding to the flashlight skill. /// The difficulty corresponding to the flashlight skill.
/// </summary> /// </summary>
@ -94,11 +100,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty); yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor); yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
} }
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values) public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
{ {
base.FromDatabaseAttributes(values); base.FromDatabaseAttributes(values, onlineInfo);
AimDifficulty = values[ATTRIB_ID_AIM]; AimDifficulty = values[ATTRIB_ID_AIM];
SpeedDifficulty = values[ATTRIB_ID_SPEED]; SpeedDifficulty = values[ATTRIB_ID_SPEED];
@ -108,6 +115,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
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];
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
DrainRate = onlineInfo.DrainRate;
HitCircleCount = onlineInfo.CircleCount;
SliderCount = onlineInfo.SliderCount;
SpinnerCount = onlineInfo.SpinnerCount;
} }
#region Newtonsoft.Json implicit ShouldSerialize() methods #region Newtonsoft.Json implicit ShouldSerialize() methods

View File

@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier; double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier; double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
double speedNotes = ((Speed)skills[2]).RelevantNoteCount();
double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier; double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
@ -75,6 +76,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Mods = mods, Mods = mods,
AimDifficulty = aimRating, AimDifficulty = aimRating,
SpeedDifficulty = speedRating, SpeedDifficulty = speedRating,
SpeedNoteCount = speedNotes,
FlashlightDifficulty = flashlightRating, FlashlightDifficulty = flashlightRating,
SliderFactor = sliderFactor, SliderFactor = sliderFactor,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,

View File

@ -163,8 +163,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
} }
// Calculate accuracy assuming the worst case scenario
double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat));
double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk));
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
// Scale the speed value with accuracy and OD. // Scale the speed value with accuracy and OD.
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2); speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping. // Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);

View File

@ -6,6 +6,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators; using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// <summary> /// <summary>
/// Represents the skill required to memorise and hit every object in a map with the Flashlight mod enabled. /// Represents the skill required to memorise and hit every object in a map with the Flashlight mod enabled.
/// </summary> /// </summary>
public class Flashlight : OsuStrainSkill public class Flashlight : StrainSkill
{ {
private readonly bool hasHiddenMod; private readonly bool hasHiddenMod;
@ -27,7 +28,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double skillMultiplier => 0.05; private double skillMultiplier => 0.05;
private double strainDecayBase => 0.15; private double strainDecayBase => 0.15;
protected override double DecayWeight => 1.0;
private double currentStrain; private double currentStrain;
@ -42,5 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return currentStrain; return currentStrain;
} }
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER;
} }
} }

View File

@ -14,6 +14,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
public abstract class OsuStrainSkill : StrainSkill public abstract class OsuStrainSkill : StrainSkill
{ {
/// <summary>
/// The default multiplier applied by <see cref="OsuStrainSkill"/> to the final difficulty value after all other calculations.
/// May be overridden via <see cref="DifficultyMultiplier"/>.
/// </summary>
public const double DEFAULT_DIFFICULTY_MULTIPLIER = 1.06;
/// <summary> /// <summary>
/// The number of sections with the highest strains, which the peak strain reductions will apply to. /// The number of sections with the highest strains, which the peak strain reductions will apply to.
/// This is done in order to decrease their impact on the overall difficulty of the map for this skill. /// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
@ -28,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// <summary> /// <summary>
/// The final multiplier to be applied to <see cref="DifficultyValue"/> after all other calculations. /// The final multiplier to be applied to <see cref="DifficultyValue"/> after all other calculations.
/// </summary> /// </summary>
protected virtual double DifficultyMultiplier => 1.06; protected virtual double DifficultyMultiplier => DEFAULT_DIFFICULTY_MULTIPLIER;
protected OsuStrainSkill(Mod[] mods) protected OsuStrainSkill(Mod[] mods)
: base(mods) : base(mods)

View File

@ -8,6 +8,8 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators; using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
@ -26,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override double DifficultyMultiplier => 1.04; protected override double DifficultyMultiplier => 1.04;
private readonly double greatWindow; private readonly double greatWindow;
private readonly List<double> objectStrains = new List<double>();
public Speed(Mod[] mods, double hitWindowGreat) public Speed(Mod[] mods, double hitWindowGreat)
: base(mods) : base(mods)
{ {
@ -43,7 +47,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current, greatWindow); currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current, greatWindow);
return currentStrain * currentRhythm; double totalStrain = currentStrain * currentRhythm;
objectStrains.Add(totalStrain);
return totalStrain;
}
public double RelevantNoteCount()
{
if (objectStrains.Count == 0)
return 0;
double maxStrain = objectStrains.Max();
if (maxStrain == 0)
return 0;
return objectStrains.Aggregate((total, next) => total + (1.0 / (1.0 + Math.Exp(-(next / maxStrain * 12.0 - 6.0)))));
} }
} }
} }

View File

@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Edit
}); });
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy(); selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid(); selectedHitObjects.CollectionChanged += (_, _) => updateDistanceSnapGrid();
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy(); placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
placementObject.ValueChanged += _ => updateDistanceSnapGrid(); placementObject.ValueChanged += _ => updateDistanceSnapGrid();
@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Edit
switch (BlueprintContainer.CurrentTool) switch (BlueprintContainer.CurrentTool)
{ {
case SelectTool _: case SelectTool:
if (!EditorBeatmap.SelectedHitObjects.Any()) if (!EditorBeatmap.SelectedHitObjects.Any())
return; return;

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableHitObject(DrawableHitObject drawable) public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{ {
drawable.ApplyCustomUpdateState += (drawableObject, state) => drawable.ApplyCustomUpdateState += (drawableObject, _) =>
{ {
if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return; if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return;

View File

@ -30,9 +30,6 @@ namespace osu.Game.Rulesets.Osu.Mods
[SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")] [SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")]
public Bindable<bool> ClassicNoteLock { get; } = new BindableBool(true); public Bindable<bool> ClassicNoteLock { get; } = new BindableBool(true);
[SettingSource("Use fixed slider follow circle hit area", "Makes the slider follow circle track its final size at all times.")]
public Bindable<bool> FixedFollowCircleHitArea { get; } = new BindableBool(true);
[SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")] [SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")]
public Bindable<bool> AlwaysPlayTailSample { get; } = new BindableBool(true); public Bindable<bool> AlwaysPlayTailSample { get; } = new BindableBool(true);
@ -62,10 +59,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
switch (obj) switch (obj)
{ {
case DrawableSlider slider:
slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
break;
case DrawableSliderHead head: case DrawableSliderHead head:
head.TrackFollowCircle = !NoSliderHeadMovement.Value; head.TrackFollowCircle = !NoSliderHeadMovement.Value;
break; break;

View File

@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawableObject) switch (drawableObject)
{ {
case DrawableSliderTail _: case DrawableSliderTail:
using (drawableObject.BeginAbsoluteSequence(fadeStartTime)) using (drawableObject.BeginAbsoluteSequence(fadeStartTime))
drawableObject.FadeOut(fadeDuration); drawableObject.FadeOut(fadeDuration);
@ -165,14 +165,14 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (hitObject) switch (hitObject)
{ {
case Slider _: case Slider:
return (fadeOutStartTime, longFadeDuration); return (fadeOutStartTime, longFadeDuration);
case SliderTick _: case SliderTick:
double tickFadeOutDuration = Math.Min(hitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000); double tickFadeOutDuration = Math.Min(hitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000);
return (hitObject.StartTime - tickFadeOutDuration, tickFadeOutDuration); return (hitObject.StartTime - tickFadeOutDuration, tickFadeOutDuration);
case Spinner _: case Spinner:
return (fadeOutStartTime + longFadeDuration, fadeOutDuration); return (fadeOutStartTime + longFadeDuration, fadeOutDuration);
default: default:

View File

@ -44,13 +44,13 @@ namespace osu.Game.Rulesets.Osu.Mods
// apply grow effect // apply grow effect
switch (drawable) switch (drawable)
{ {
case DrawableSliderHead _: case DrawableSliderHead:
case DrawableSliderTail _: case DrawableSliderTail:
// special cases we should *not* be scaling. // special cases we should *not* be scaling.
break; break;
case DrawableSlider _: case DrawableSlider:
case DrawableHitCircle _: case DrawableHitCircle:
{ {
using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
drawable.ScaleTo(StartScale.Value).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine); drawable.ScaleTo(StartScale.Value).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine);

View File

@ -34,10 +34,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
switch (drawable) switch (drawable)
{ {
case DrawableSliderHead _: case DrawableSliderHead:
case DrawableSliderTail _: case DrawableSliderTail:
case DrawableSliderTick _: case DrawableSliderTick:
case DrawableSliderRepeat _: case DrawableSliderRepeat:
return; return;
default: default:

View File

@ -29,7 +29,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public DrawableSliderHead HeadCircle => headContainer.Child; public DrawableSliderHead HeadCircle => headContainer.Child;
public DrawableSliderTail TailCircle => tailContainer.Child; public DrawableSliderTail TailCircle => tailContainer.Child;
public SliderBall Ball { get; private set; } [Cached]
public DrawableSliderBall Ball { get; private set; }
public SkinnableDrawable Body { get; private set; } public SkinnableDrawable Body { get; private set; }
/// <summary> /// <summary>
@ -60,6 +62,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public DrawableSlider([CanBeNull] Slider s = null) public DrawableSlider([CanBeNull] Slider s = null)
: base(s) : base(s)
{ {
Ball = new DrawableSliderBall
{
GetInitialHitAction = () => HeadCircle.HitAction,
BypassAutoSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0
};
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -73,13 +82,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both }, repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both }, headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, }, OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
Ball = new SliderBall(this) Ball,
{
GetInitialHitAction = () => HeadCircle.HitAction,
BypassAutoSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0
},
slidingSample = new PausableSkinnableSound { Looping = true } slidingSample = new PausableSkinnableSound { Looping = true }
}; };

View File

@ -7,24 +7,21 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
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.Objects; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Default namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
{ {
public Func<OsuAction?> GetInitialHitAction; public Func<OsuAction?> GetInitialHitAction;
@ -34,19 +31,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
set => ball.Colour = value; set => ball.Colour = value;
} }
/// <summary> private Drawable followCircle;
/// Whether to track accurately to the visual size of this <see cref="SliderBall"/>. private Drawable followCircleReceptor;
/// If <c>false</c>, tracking will be performed at the final scale at all times. private DrawableSlider drawableSlider;
/// </summary> private Drawable ball;
public bool InputTracksVisualSize = true;
private readonly Drawable followCircle; [BackgroundDependencyLoader]
private readonly DrawableSlider drawableSlider; private void load(DrawableHitObject drawableSlider)
private readonly Drawable ball;
public SliderBall(DrawableSlider drawableSlider)
{ {
this.drawableSlider = drawableSlider; this.drawableSlider = (DrawableSlider)drawableSlider;
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -54,13 +47,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Children = new[] Children = new[]
{ {
followCircle = new FollowCircleContainer followCircle = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle())
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Alpha = 0, Alpha = 0,
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()), },
followCircleReceptor = new CircularContainer
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true
}, },
ball = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall()) ball = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall())
{ {
@ -104,14 +103,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
tracking = value; tracking = value;
if (InputTracksVisualSize) followCircleReceptor.Scale = new Vector2(tracking ? 2.4f : 1f);
followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
else
{
// We need to always be tracking the final size, at both endpoints. For now, this is achieved by removing the scale duration.
followCircle.ScaleTo(tracking ? 2.4f : 1f);
}
followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint); followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint);
} }
} }
@ -170,7 +164,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
// in valid time range // in valid time range
Time.Current >= drawableSlider.HitObject.StartTime && Time.Current < drawableSlider.HitObject.EndTime && Time.Current >= drawableSlider.HitObject.StartTime && Time.Current < drawableSlider.HitObject.EndTime &&
// in valid position range // in valid position range
lastScreenSpaceMousePosition.HasValue && followCircle.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) && lastScreenSpaceMousePosition.HasValue && followCircleReceptor.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) &&
// valid action // valid action
(actions?.Any(isValidTrackingAction) ?? false); (actions?.Any(isValidTrackingAction) ?? false);
@ -208,74 +202,5 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
lastPosition = Position; lastPosition = Position;
} }
private class FollowCircleContainer : CircularContainer
{
public override bool HandlePositionalInput => true;
}
public class DefaultFollowCircle : CompositeDrawable
{
public DefaultFollowCircle()
{
RelativeSizeAxes = Axes.Both;
InternalChild = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 5,
BorderColour = Color4.Orange,
Blending = BlendingParameters.Additive,
Child = new Box
{
Colour = Color4.Orange,
RelativeSizeAxes = Axes.Both,
Alpha = 0.2f,
}
};
}
}
public class DefaultSliderBall : CompositeDrawable
{
private Box box;
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject, ISkinSource skin)
{
var slider = (DrawableSlider)drawableObject;
RelativeSizeAxes = Axes.Both;
float radius = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS;
InternalChild = new CircularContainer
{
Masking = true,
RelativeSizeAxes = Axes.Both,
Scale = new Vector2(radius / OsuHitObject.OBJECT_RADIUS),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
BorderThickness = 10,
BorderColour = Color4.White,
Alpha = 1,
Child = box = new Box
{
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
AlwaysPresent = true,
Alpha = 0
}
};
slider.Tracking.BindValueChanged(trackingChanged, true);
}
private void trackingChanged(ValueChangedEvent<bool> tracking) =>
box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint);
}
} }
} }

View File

@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public Slider() public Slider()
{ {
SamplesBindable.CollectionChanged += (_, __) => UpdateNestedSamples(); SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
Path.Version.ValueChanged += _ => updateNestedPositions(); Path.Version.ValueChanged += _ => updateNestedPositions();
} }

View File

@ -120,19 +120,19 @@ namespace osu.Game.Rulesets.Osu
{ {
switch (mod) switch (mod)
{ {
case OsuModAutopilot _: case OsuModAutopilot:
value |= LegacyMods.Autopilot; value |= LegacyMods.Autopilot;
break; break;
case OsuModSpunOut _: case OsuModSpunOut:
value |= LegacyMods.SpunOut; value |= LegacyMods.SpunOut;
break; break;
case OsuModTarget _: case OsuModTarget:
value |= LegacyMods.Target; value |= LegacyMods.Target;
break; break;
case OsuModTouchDevice _: case OsuModTouchDevice:
value |= LegacyMods.TouchDevice; value |= LegacyMods.TouchDevice;
break; break;
} }

View File

@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Osu
protected override string RulesetPrefix => OsuRuleset.SHORT_NAME; protected override string RulesetPrefix => OsuRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLower(); protected override string ComponentName => Component.ToString().ToLowerInvariant();
} }
} }

View File

@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Replays
hitWindows = slider.TailCircle.HitWindows; hitWindows = slider.TailCircle.HitWindows;
break; break;
case Spinner _: case Spinner:
hitWindows = defaultHitWindows; hitWindows = defaultHitWindows;
break; break;
} }

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
{ {
switch (hitObject) switch (hitObject)
{ {
case HitCircle _: case HitCircle:
return new OsuHitCircleJudgementResult(hitObject, judgement); return new OsuHitCircleJudgementResult(hitObject, judgement);
default: default:

View File

@ -0,0 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class DefaultFollowCircle : CompositeDrawable
{
public DefaultFollowCircle()
{
RelativeSizeAxes = Axes.Both;
InternalChild = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 5,
BorderColour = Color4.Orange,
Blending = BlendingParameters.Additive,
Child = new Box
{
Colour = Color4.Orange,
RelativeSizeAxes = Axes.Both,
Alpha = 0.2f,
}
};
}
}
}

View File

@ -0,0 +1,60 @@
// 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.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class DefaultSliderBall : CompositeDrawable
{
private Box box;
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject, ISkinSource skin)
{
var slider = (DrawableSlider)drawableObject;
RelativeSizeAxes = Axes.Both;
float radius = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS;
InternalChild = new CircularContainer
{
Masking = true,
RelativeSizeAxes = Axes.Both,
Scale = new Vector2(radius / OsuHitObject.OBJECT_RADIUS),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
BorderThickness = 10,
BorderColour = Color4.White,
Alpha = 1,
Child = box = new Box
{
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
AlwaysPresent = true,
Alpha = 0
}
};
slider.Tracking.BindValueChanged(trackingChanged, true);
}
private void trackingChanged(ValueChangedEvent<bool> tracking) =>
box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint);
}
}

View 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.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public class LegacyFollowCircle : CompositeDrawable
{
public LegacyFollowCircle(Drawable animationContent)
{
// follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x
animationContent.Scale *= 0.5f;
animationContent.Anchor = Anchor.Centre;
animationContent.Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
InternalChild = animationContent;
}
}
}

View File

@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
break; break;
case DrawableSpinnerBonusTick _: case DrawableSpinnerBonusTick:
if (state == ArmedState.Hit) if (state == ArmedState.Hit)
glow.FlashColour(Color4.White, 200); glow.FlashColour(Color4.White, 200);

View File

@ -41,11 +41,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return this.GetAnimation(component.LookupName, false, false); return this.GetAnimation(component.LookupName, false, false);
case OsuSkinComponents.SliderFollowCircle: case OsuSkinComponents.SliderFollowCircle:
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true); var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true);
if (followCircle != null) if (followCircleContent != null)
// follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x return new LegacyFollowCircle(followCircleContent);
followCircle.Scale *= 0.5f;
return followCircle; return null;
case OsuSkinComponents.SliderBall: case OsuSkinComponents.SliderBall:
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: ""); var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "");

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.UI
switch (obj) switch (obj)
{ {
case DrawableSpinner _: case DrawableSpinner:
continue; continue;
case DrawableSlider slider: case DrawableSlider slider:

View File

@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.UI
// note: `Slider`'s `ProxiedLayer` is added when its nested `DrawableHitCircle` is loaded. // note: `Slider`'s `ProxiedLayer` is added when its nested `DrawableHitCircle` is loaded.
switch (drawable) switch (drawable)
{ {
case DrawableSpinner _: case DrawableSpinner:
spinnerProxies.Add(drawable.CreateProxy()); spinnerProxies.Add(drawable.CreateProxy());
break; break;

View File

@ -88,11 +88,11 @@ namespace osu.Game.Rulesets.Osu.Utils
switch (hitObject) switch (hitObject)
{ {
case HitCircle _: case HitCircle:
shift = clampHitCircleToPlayfield(current); shift = clampHitCircleToPlayfield(current);
break; break;
case Slider _: case Slider:
shift = clampSliderToPlayfield(current); shift = clampSliderToPlayfield(current);
break; break;
} }

View File

@ -144,7 +144,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
createDrawableRuleset(); createDrawableRuleset();
assertStateAfterResult(new JudgementResult(new Swell(), new TaikoSwellJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Clear); assertStateAfterResult(new JudgementResult(new Swell(), new TaikoSwellJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Clear);
AddUntilStep($"state reverts to {expectedStateAfterClear.ToString().ToLower()}", () => allMascotsIn(expectedStateAfterClear)); AddUntilStep($"state reverts to {expectedStateAfterClear.ToString().ToLowerInvariant()}", () => allMascotsIn(expectedStateAfterClear));
} }
private void setBeatmap(bool kiai = false) private void setBeatmap(bool kiai = false)
@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{ {
TaikoMascotAnimationState[] mascotStates = null; TaikoMascotAnimationState[] mascotStates = null;
AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}", AddStep($"{judgementResult.Type.ToString().ToLowerInvariant()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
() => () =>
{ {
applyNewResult(judgementResult); applyNewResult(judgementResult);
@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Schedule(() => mascotStates = animatedMascots.Select(mascot => mascot.State.Value).ToArray()); Schedule(() => mascotStates = animatedMascots.Select(mascot => mascot.State.Value).ToArray());
}); });
AddAssert($"state is {expectedState.ToString().ToLower()}", () => mascotStates.All(state => state == expectedState)); AddAssert($"state is {expectedState.ToString().ToLowerInvariant()}", () => mascotStates.All(state => state == expectedState));
} }
private void applyNewResult(JudgementResult judgementResult) private void applyNewResult(JudgementResult judgementResult)

View File

@ -24,22 +24,25 @@ namespace osu.Game.Rulesets.Taiko.Tests
new object[] { LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime) } }, new object[] { LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime) } },
new object[] { LegacyMods.Relax, new[] { typeof(TaikoModRelax) } }, new object[] { LegacyMods.Relax, new[] { typeof(TaikoModRelax) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(TaikoModHalfTime) } }, new object[] { LegacyMods.HalfTime, new[] { typeof(TaikoModHalfTime) } },
new object[] { LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) } },
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.Perfect, new[] { typeof(TaikoModPerfect) } },
new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } }, new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } },
new object[] { LegacyMods.Cinema, new[] { typeof(TaikoModCinema) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } } new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } }
}; };
[TestCaseSource(nameof(taiko_mod_mapping))]
[TestCase(LegacyMods.Cinema, new[] { typeof(TaikoModCinema) })]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(TaikoModCinema) })]
[TestCase(LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModNightcore) })]
[TestCase(LegacyMods.Perfect, new[] { typeof(TaikoModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(TaikoModPerfect) })]
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(taiko_mod_mapping))] [TestCaseSource(nameof(taiko_mod_mapping))]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(TaikoModCinema) })] [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(TaikoModCinema) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModNightcore) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModNightcore) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(TaikoModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(TaikoModPerfect) })]
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(taiko_mod_mapping))]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new TaikoRuleset(); protected override Ruleset CreateRuleset() => new TaikoRuleset();

View File

@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddStep("Setup judgements", () => AddStep("Setup judgements", () =>
{ {
judged = false; judged = false;
Player.ScoreProcessor.NewJudgement += b => judged = true; Player.ScoreProcessor.NewJudgement += _ => judged = true;
}); });
AddUntilStep("swell judged", () => judged); AddUntilStep("swell judged", () => judged);
AddAssert("failed", () => Player.GameplayState.HasFailed); AddAssert("failed", () => Player.GameplayState.HasFailed);

View File

@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Taiko.Difficulty namespace osu.Game.Rulesets.Taiko.Difficulty
@ -57,9 +56,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
} }
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values) public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
{ {
base.FromDatabaseAttributes(values); base.FromDatabaseAttributes(values, onlineInfo);
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
StarRating = values[ATTRIB_ID_DIFFICULTY]; StarRating = values[ATTRIB_ID_DIFFICULTY];

View File

@ -48,8 +48,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
switch (hitObject) switch (hitObject)
{ {
case DrawableDrumRollTick _: case DrawableDrumRollTick:
case DrawableHit _: case DrawableHit:
double preempt = drawableRuleset.TimeRange.Value / drawableRuleset.ControlPointAt(hitObject.HitObject.StartTime).Multiplier; double preempt = drawableRuleset.TimeRange.Value / drawableRuleset.ControlPointAt(hitObject.HitObject.StartTime).Multiplier;
double start = hitObject.HitObject.StartTime - preempt * fade_out_start_time; double start = hitObject.HitObject.StartTime - preempt * fade_out_start_time;
double duration = preempt * fade_out_duration; double duration = preempt * fade_out_duration;

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
DisplayColour.Value = Type == HitType.Centre ? COLOUR_CENTRE : COLOUR_RIM; DisplayColour.Value = Type == HitType.Centre ? COLOUR_CENTRE : COLOUR_RIM;
}); });
SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples()); SamplesBindable.BindCollectionChanged((_, _) => updateTypeFromSamples());
} }
private void updateTypeFromSamples() private void updateTypeFromSamples()

View File

@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
protected TaikoStrongableHitObject() protected TaikoStrongableHitObject()
{ {
IsStrongBindable.BindValueChanged(_ => updateSamplesFromType()); IsStrongBindable.BindValueChanged(_ => updateSamplesFromType());
SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples()); SamplesBindable.BindCollectionChanged((_, _) => updateTypeFromSamples());
} }
private void updateTypeFromSamples() private void updateTypeFromSamples()

View File

@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Taiko
protected override string RulesetPrefix => TaikoRuleset.SHORT_NAME; protected override string RulesetPrefix => TaikoRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLower(); protected override string ComponentName => Component.ToString().ToLowerInvariant();
} }
} }

View File

@ -139,10 +139,10 @@ namespace osu.Game.Rulesets.Taiko.UI
private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex) private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex)
{ {
var texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}"); var texture = skin.GetTexture($"pippidon{state.ToString().ToLowerInvariant()}{frameIndex}");
if (frameIndex == 0 && texture == null) if (frameIndex == 0 && texture == null)
texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}"); texture = skin.GetTexture($"pippidon{state.ToString().ToLowerInvariant()}");
return texture; return texture;
} }

View File

@ -245,7 +245,7 @@ namespace osu.Game.Rulesets.Taiko.UI
barLinePlayfield.Add(barLine); barLinePlayfield.Add(barLine);
break; break;
case DrawableTaikoHitObject _: case DrawableTaikoHitObject:
base.Add(h); base.Add(h);
break; break;
@ -261,7 +261,7 @@ namespace osu.Game.Rulesets.Taiko.UI
case DrawableBarLine barLine: case DrawableBarLine barLine:
return barLinePlayfield.Remove(barLine); return barLinePlayfield.Remove(barLine);
case DrawableTaikoHitObject _: case DrawableTaikoHitObject:
return base.Remove(h); return base.Remove(h);
default: default:
@ -280,12 +280,12 @@ namespace osu.Game.Rulesets.Taiko.UI
switch (result.Judgement) switch (result.Judgement)
{ {
case TaikoStrongJudgement _: case TaikoStrongJudgement:
if (result.IsHit) if (result.IsHit)
hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).ParentHitObject)?.VisualiseSecondHit(result); hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).ParentHitObject)?.VisualiseSecondHit(result);
break; break;
case TaikoDrumRollTickJudgement _: case TaikoDrumRollTickJudgement:
if (!result.IsHit) if (!result.IsHit)
break; break;

View File

@ -157,7 +157,7 @@ namespace osu.Game.Tests.Beatmaps
[TestCase(8.3, DifficultyRating.ExpertPlus)] [TestCase(8.3, DifficultyRating.ExpertPlus)]
public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket) public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
{ {
var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating); var actualBracket = StarDifficulty.GetDifficultyRating(starRating);
Assert.AreEqual(expectedBracket, actualBracket); Assert.AreEqual(expectedBracket, actualBracket);
} }

View File

@ -99,7 +99,7 @@ namespace osu.Game.Tests.Collections.IO
public async Task TestImportMalformedDatabase() public async Task TestImportMalformedDatabase()
{ {
bool exceptionThrown = false; bool exceptionThrown = false;
UnhandledExceptionEventHandler setException = (_, __) => exceptionThrown = true; UnhandledExceptionEventHandler setException = (_, _) => exceptionThrown = true;
using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {

View File

@ -607,6 +607,12 @@ namespace osu.Game.Tests.Database
using (var outStream = File.Open(brokenTempFilename, FileMode.CreateNew)) using (var outStream = File.Open(brokenTempFilename, FileMode.CreateNew))
using (var zip = ZipArchive.Open(brokenOsz)) using (var zip = ZipArchive.Open(brokenOsz))
{ {
foreach (var entry in zip.Entries.ToArray())
{
if (entry.Key.EndsWith(".osu", StringComparison.InvariantCulture))
zip.RemoveEntry(entry);
}
zip.AddEntry("broken.osu", brokenOsu, false); zip.AddEntry("broken.osu", brokenOsu, false);
zip.SaveTo(outStream, CompressionType.Deflate); zip.SaveTo(outStream, CompressionType.Deflate);
} }
@ -627,7 +633,7 @@ namespace osu.Game.Tests.Database
checkSingleReferencedFileCount(realm.Realm, 18); checkSingleReferencedFileCount(realm.Realm, 18);
Assert.AreEqual(1, loggedExceptionCount); Assert.AreEqual(0, loggedExceptionCount);
File.Delete(brokenTempFilename); File.Delete(brokenTempFilename);
}); });

View File

@ -2,11 +2,14 @@
// 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 System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Database namespace osu.Game.Tests.Database
{ {
@ -33,6 +36,85 @@ namespace osu.Game.Tests.Database
}); });
} }
[Test]
public void TestAsyncWriteAsync()
{
RunTestWithRealmAsync(async (realm, _) =>
{
await realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
realm.Run(r => r.Refresh());
Assert.That(realm.Run(r => r.All<BeatmapSetInfo>().Count()), Is.EqualTo(1));
});
}
[Test]
public void TestAsyncWriteWhileBlocking()
{
RunTestWithRealm((realm, _) =>
{
Task writeTask;
using (realm.BlockAllOperations())
{
writeTask = realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
Thread.Sleep(100);
Assert.That(writeTask.IsCompleted, Is.False);
}
writeTask.WaitSafely();
realm.Run(r => r.Refresh());
Assert.That(realm.Run(r => r.All<BeatmapSetInfo>().Count()), Is.EqualTo(1));
});
}
[Test]
public void TestAsyncWrite()
{
RunTestWithRealm((realm, _) =>
{
realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())).WaitSafely();
realm.Run(r => r.Refresh());
Assert.That(realm.Run(r => r.All<BeatmapSetInfo>().Count()), Is.EqualTo(1));
});
}
[Test]
public void TestAsyncWriteAfterDisposal()
{
RunTestWithRealm((realm, _) =>
{
realm.Dispose();
Assert.ThrowsAsync<ObjectDisposedException>(() => realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())));
});
}
[Test]
public void TestAsyncWriteBeforeDisposal()
{
ManualResetEventSlim resetEvent = new ManualResetEventSlim();
RunTestWithRealm((realm, _) =>
{
var writeTask = realm.WriteAsync(r =>
{
// ensure that disposal blocks for our execution
Assert.That(resetEvent.Wait(100), Is.False);
r.Add(TestResources.CreateTestBeatmapSetInfo());
});
realm.Dispose();
resetEvent.Set();
writeTask.WaitSafely();
});
}
/// <summary> /// <summary>
/// Test to ensure that a `CreateContext` call nested inside a subscription doesn't cause any deadlocks /// Test to ensure that a `CreateContext` call nested inside a subscription doesn't cause any deadlocks
/// due to context fetching semaphores. /// due to context fetching semaphores.
@ -46,7 +128,7 @@ namespace osu.Game.Tests.Database
realm.RegisterCustomSubscription(r => realm.RegisterCustomSubscription(r =>
{ {
var subscription = r.All<BeatmapInfo>().QueryAsyncWithNotifications((sender, changes, error) => var subscription = r.All<BeatmapInfo>().QueryAsyncWithNotifications((_, _, _) =>
{ {
realm.Run(_ => realm.Run(_ =>
{ {
@ -79,11 +161,11 @@ namespace osu.Game.Tests.Database
{ {
hasThreadedUsage.Set(); hasThreadedUsage.Set();
stopThreadedUsage.Wait(); stopThreadedUsage.Wait(60000);
}); });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler);
hasThreadedUsage.Wait(); hasThreadedUsage.Wait(60000);
Assert.Throws<TimeoutException>(() => Assert.Throws<TimeoutException>(() =>
{ {

View File

@ -91,6 +91,25 @@ namespace osu.Game.Tests.Database
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
} }
[Test]
public void TestTransactionRolledBackOnException()
{
RunTestWithRealm((realm, _) =>
{
var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
realm.Run(r => r.Write(_ => r.Add(beatmap)));
var liveBeatmap = beatmap.ToLive(realm);
Assert.Throws<InvalidOperationException>(() => liveBeatmap.PerformWrite(l => throw new InvalidOperationException()));
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
liveBeatmap.PerformWrite(l => l.Hidden = true);
Assert.IsTrue(liveBeatmap.PerformRead(l => l.Hidden));
});
}
[Test] [Test]
public void TestScopedReadWithoutContext() public void TestScopedReadWithoutContext()
{ {
@ -189,7 +208,7 @@ namespace osu.Game.Tests.Database
}); });
// Can't be used, even from within a valid context. // Can't be used, even from within a valid context.
realm.Run(threadContext => realm.Run(_ =>
{ {
Assert.Throws<InvalidOperationException>(() => Assert.Throws<InvalidOperationException>(() =>
{ {

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
@ -84,11 +83,7 @@ namespace osu.Game.Tests.Database
realm.Run(r => r.Refresh()); realm.Run(r => r.Refresh());
// Without forcing the write onto its own thread, realm will internally run the operation synchronously, which can cause a deadlock with `WaitSafely`. realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())).WaitSafely();
Task.Run(async () =>
{
await realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
}).WaitSafely();
realm.Run(r => r.Refresh()); realm.Run(r => r.Refresh());

View File

@ -192,7 +192,7 @@ namespace osu.Game.Tests.Gameplay
AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect })); AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect }));
AddAssert("not failed", () => !processor.HasFailed); AddAssert("not failed", () => !processor.HasFailed);
AddStep($"apply {resultApplied.ToString().ToLower()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied })); AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied }));
AddAssert("failed", () => processor.HasFailed); AddAssert("failed", () => processor.HasFailed);
} }

View File

@ -44,8 +44,7 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestCustomDirectory() public void TestCustomDirectory()
{ {
string customPath = prepareCustomPath(); using (prepareCustomPath(out string customPath))
using (var host = new CustomTestHeadlessGameHost()) using (var host = new CustomTestHeadlessGameHost())
{ {
using (var storageConfig = new StorageConfigManager(host.InitialStorage)) using (var storageConfig = new StorageConfigManager(host.InitialStorage))
@ -63,7 +62,6 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
} }
} }
} }
@ -71,8 +69,7 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestSubDirectoryLookup() public void TestSubDirectoryLookup()
{ {
string customPath = prepareCustomPath(); using (prepareCustomPath(out string customPath))
using (var host = new CustomTestHeadlessGameHost()) using (var host = new CustomTestHeadlessGameHost())
{ {
using (var storageConfig = new StorageConfigManager(host.InitialStorage)) using (var storageConfig = new StorageConfigManager(host.InitialStorage))
@ -97,7 +94,6 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
} }
} }
} }
@ -105,8 +101,7 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestMigration() public void TestMigration()
{ {
string customPath = prepareCustomPath(); using (prepareCustomPath(out string customPath))
using (var host = new CustomTestHeadlessGameHost()) using (var host = new CustomTestHeadlessGameHost())
{ {
try try
@ -173,7 +168,6 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
} }
} }
} }
@ -181,9 +175,8 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestMigrationBetweenTwoTargets() public void TestMigrationBetweenTwoTargets()
{ {
string customPath = prepareCustomPath(); using (prepareCustomPath(out string customPath))
string customPath2 = prepareCustomPath(); using (prepareCustomPath(out string customPath2))
using (var host = new CustomTestHeadlessGameHost()) using (var host = new CustomTestHeadlessGameHost())
{ {
try try
@ -205,8 +198,6 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
cleanupPath(customPath2);
} }
} }
} }
@ -214,8 +205,7 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestMigrationToSameTargetFails() public void TestMigrationToSameTargetFails()
{ {
string customPath = prepareCustomPath(); using (prepareCustomPath(out string customPath))
using (var host = new CustomTestHeadlessGameHost()) using (var host = new CustomTestHeadlessGameHost())
{ {
try try
@ -228,7 +218,6 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
} }
} }
} }
@ -236,9 +225,8 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestMigrationFailsOnExistingData() public void TestMigrationFailsOnExistingData()
{ {
string customPath = prepareCustomPath(); using (prepareCustomPath(out string customPath))
string customPath2 = prepareCustomPath(); using (prepareCustomPath(out string customPath2))
using (var host = new CustomTestHeadlessGameHost()) using (var host = new CustomTestHeadlessGameHost())
{ {
try try
@ -254,7 +242,7 @@ namespace osu.Game.Tests.NonVisual
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME))); Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
Directory.CreateDirectory(customPath2); Directory.CreateDirectory(customPath2);
File.Copy(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME), Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME)); File.WriteAllText(Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME), "I am a text");
// Fails because file already exists. // Fails because file already exists.
Assert.Throws<ArgumentException>(() => osu.Migrate(customPath2)); Assert.Throws<ArgumentException>(() => osu.Migrate(customPath2));
@ -267,8 +255,6 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
cleanupPath(customPath2);
} }
} }
} }
@ -276,8 +262,7 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestMigrationToNestedTargetFails() public void TestMigrationToNestedTargetFails()
{ {
string customPath = prepareCustomPath(); using (prepareCustomPath(out string customPath))
using (var host = new CustomTestHeadlessGameHost()) using (var host = new CustomTestHeadlessGameHost())
{ {
try try
@ -298,7 +283,6 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
} }
} }
} }
@ -306,8 +290,7 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestMigrationToSeeminglyNestedTarget() public void TestMigrationToSeeminglyNestedTarget()
{ {
string customPath = prepareCustomPath(); using (prepareCustomPath(out string customPath))
using (var host = new CustomTestHeadlessGameHost()) using (var host = new CustomTestHeadlessGameHost())
{ {
try try
@ -328,7 +311,6 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
} }
} }
} }
@ -343,14 +325,17 @@ namespace osu.Game.Tests.NonVisual
return path; return path;
} }
private static string prepareCustomPath() => Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, $"custom-path-{Guid.NewGuid()}"); private static IDisposable prepareCustomPath(out string path)
{
path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, $"custom-path-{Guid.NewGuid()}");
return new InvokeOnDisposal<string>(path, cleanupPath);
}
private static void cleanupPath(string path) private static void cleanupPath(string path)
{ {
try try
{ {
if (Directory.Exists(path)) if (Directory.Exists(path)) Directory.Delete(path, true);
Directory.Delete(path, true);
} }
catch catch
{ {

View File

@ -83,14 +83,14 @@ namespace osu.Game.Tests.NonVisual
public override event Action<JudgementResult> NewResult public override event Action<JudgementResult> NewResult
{ {
add => throw new InvalidOperationException(); add => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context");
remove => throw new InvalidOperationException(); remove => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context");
} }
public override event Action<JudgementResult> RevertResult public override event Action<JudgementResult> RevertResult
{ {
add => throw new InvalidOperationException(); add => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context");
remove => throw new InvalidOperationException(); remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context");
} }
public override Playfield Playfield { get; } public override Playfield Playfield { get; }

View File

@ -364,12 +364,12 @@ namespace osu.Game.Tests.NonVisual
private void confirmCurrentFrame(int? frame) private void confirmCurrentFrame(int? frame)
{ {
Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.CurrentFrame?.Time, "Unexpected current frame"); Assert.AreEqual(frame is int x ? replay.Frames[x].Time : null, handler.CurrentFrame?.Time, "Unexpected current frame");
} }
private void confirmNextFrame(int? frame) private void confirmNextFrame(int? frame)
{ {
Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.NextFrame?.Time, "Unexpected next frame"); Assert.AreEqual(frame is int x ? replay.Frames[x].Time : null, handler.NextFrame?.Time, "Unexpected next frame");
} }
private class TestReplayFrame : ReplayFrame private class TestReplayFrame : ReplayFrame

View File

@ -157,7 +157,7 @@ namespace osu.Game.Tests.NonVisual.Skinning
{ {
// use an incrementing width to allow assertion matching on correct textures as they turn from uploads into actual textures. // use an incrementing width to allow assertion matching on correct textures as they turn from uploads into actual textures.
int width = 1; int width = 1;
Textures = fileNames.ToDictionary(fileName => fileName, fileName => new TextureUpload(new Image<Rgba32>(width, width++))); Textures = fileNames.ToDictionary(fileName => fileName, _ => new TextureUpload(new Image<Rgba32>(width, width++)));
} }
public TextureUpload Get(string name) => Textures.GetValueOrDefault(name); public TextureUpload Get(string name) => Textures.GetValueOrDefault(name);

View File

@ -58,7 +58,7 @@ namespace osu.Game.Tests.Skins
{ {
AddStep($"Set beatmap skin enabled to {allowBeatmapLookups}", () => config.SetValue(OsuSetting.BeatmapSkins, allowBeatmapLookups)); AddStep($"Set beatmap skin enabled to {allowBeatmapLookups}", () => config.SetValue(OsuSetting.BeatmapSkins, allowBeatmapLookups));
ISkin expected() => allowBeatmapLookups ? (ISkin)beatmapSource : userSource; ISkin expected() => allowBeatmapLookups ? beatmapSource : userSource;
AddAssert("Check lookup is from correct source", () => requester.FindProvider(s => s.GetDrawableComponent(new TestSkinComponent()) != null) == expected()); AddAssert("Check lookup is from correct source", () => requester.FindProvider(s => s.GetDrawableComponent(new TestSkinComponent()) != null) == expected());
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Utils; using osu.Game.Utils;

View File

@ -36,8 +36,6 @@ namespace osu.Game.Tests.Visual.Editing
{ {
protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override bool EditorComponentsReady => Editor.ChildrenOfType<SetupScreen>().SingleOrDefault()?.IsLoaded == true;
protected override bool IsolateSavingFromDatabase => false; protected override bool IsolateSavingFromDatabase => false;
[Resolved] [Resolved]
@ -95,18 +93,23 @@ namespace osu.Game.Tests.Visual.Editing
string extractedFolder = $"{temp}_extracted"; string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder); Directory.CreateDirectory(extractedFolder);
try
{
using (var zip = ZipArchive.Open(temp)) using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder); zip.WriteToDirectory(extractedFolder);
bool success = setup.ChildrenOfType<ResourcesSection>().First().ChangeAudioTrack(new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3"))); bool success = setup.ChildrenOfType<ResourcesSection>().First().ChangeAudioTrack(new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")));
File.Delete(temp);
Directory.Delete(extractedFolder, true);
// ensure audio file is copied to beatmap as "audio.mp3" rather than original filename. // ensure audio file is copied to beatmap as "audio.mp3" rather than original filename.
Assert.That(Beatmap.Value.Metadata.AudioFile == "audio.mp3"); Assert.That(Beatmap.Value.Metadata.AudioFile == "audio.mp3");
return success; return success;
}
finally
{
File.Delete(temp);
Directory.Delete(extractedFolder, true);
}
}); });
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000); AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);

View File

@ -6,17 +6,14 @@ using NUnit.Framework;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
{ {
@ -38,15 +35,18 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo); AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded); AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
AddStep("test gameplay", () => AddStep("test gameplay", () => ((Editor)Game.ScreenStack.CurrentScreen).TestGameplay());
AddUntilStep("wait for player", () =>
{ {
var testGameplayButton = this.ChildrenOfType<TestGameplayButton>().Single(); // notifications may fire at almost any inopportune time and cause annoying test failures.
InputManager.MoveMouseTo(testGameplayButton); // relentlessly attempt to dismiss any and all interfering overlays, which includes notifications.
InputManager.Click(MouseButton.Left); // this is theoretically not foolproof, but it's the best that can be done here.
Game.CloseAllOverlays();
return Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded;
}); });
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded);
AddAssert("current ruleset is osu!", () => Game.Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); AddAssert("current ruleset is osu!", () => Game.Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield())); AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));

View File

@ -6,10 +6,10 @@
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Break; using osu.Game.Screens.Play.Break;
@ -31,19 +31,20 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void AddCheckSteps() protected override void AddCheckSteps()
{ {
// It doesn't matter which ruleset is used - this beatmap is only used for reference.
var beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2));
seekToBreak(0);
seekTo(beatmap.Beatmap.Breaks[0].StartTime);
AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting);
AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType<BreakInfo>().Single().AccuracyDisplay.Current.Value == 1); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType<BreakInfo>().Single().AccuracyDisplay.Current.Value == 1);
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
seekToBreak(0); seekTo(beatmap.Beatmap.HitObjects[^1].GetEndTime());
seekToBreak(1);
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true); AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true);
AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100); AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100);
@ -58,12 +59,18 @@ namespace osu.Game.Tests.Visual.Gameplay
ResultsScreen getResultsScreen() => Stack.CurrentScreen as ResultsScreen; ResultsScreen getResultsScreen() => Stack.CurrentScreen as ResultsScreen;
} }
private void seekToBreak(int breakIndex) private void seekTo(double time)
{ {
AddStep($"seek to break {breakIndex}", () => Player.GameplayClockContainer.Seek(destBreak().StartTime)); AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to complete", () => Player.DrawableRuleset.FrameStableClock.CurrentTime >= destBreak().StartTime);
BreakPeriod destBreak() => Beatmap.Value.Beatmap.Breaks.ElementAt(breakIndex); // Prevent test timeouts by seeking in 10 second increments.
for (double t = 0; t < time; t += 10000)
{
double expectedTime = t;
AddUntilStep($"current time >= {t}", () => Player.DrawableRuleset.FrameStableClock.CurrentTime >= expectedTime);
}
AddUntilStep("wait for seek to complete", () => Player.DrawableRuleset.FrameStableClock.CurrentTime >= time);
} }
} }
} }

View File

@ -322,8 +322,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
switch (h) switch (h)
{ {
case TestPooledHitObject _: case TestPooledHitObject:
case TestPooledParentHitObject _: case TestPooledParentHitObject:
return null; return null;
case TestParentHitObject p: case TestParentHitObject p:

View File

@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
HealthProcessor.FailConditions += (_, __) => true; HealthProcessor.FailConditions += (_, _) => true;
} }
private double lastFrequency = double.MaxValue; private double lastFrequency = double.MaxValue;

View File

@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
HealthProcessor.FailConditions += (_, __) => true; HealthProcessor.FailConditions += (_, _) => true;
} }
} }
} }

View File

@ -273,14 +273,14 @@ namespace osu.Game.Tests.Visual.Gameplay
public override event Action<JudgementResult> NewResult public override event Action<JudgementResult> NewResult
{ {
add => throw new InvalidOperationException(); add => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context");
remove => throw new InvalidOperationException(); remove => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context");
} }
public override event Action<JudgementResult> RevertResult public override event Action<JudgementResult> RevertResult
{ {
add => throw new InvalidOperationException(); add => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context");
remove => throw new InvalidOperationException(); remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context");
} }
public override Playfield Playfield { get; } public override Playfield Playfield { get; }

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestToggleEditor() public void TestToggleEditor()
{ {
AddToggleStep("toggle editor visibility", visible => skinEditor.ToggleVisibility()); AddToggleStep("toggle editor visibility", _ => skinEditor.ToggleVisibility());
} }
[Test] [Test]

View File

@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Child = new SkinProvidingContainer(secondarySource) Child = new SkinProvidingContainer(secondarySource)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation")) Child = consumer = new SkinConsumer("test", _ => new NamedBox("Default Implementation"))
} }
}; };
}); });
@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}; };
}); });
AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation")))); AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", _ => new NamedBox("Default Implementation"))));
AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox);
AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1); AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1);
} }
@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}; };
}); });
AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation")))); AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", _ => new NamedBox("Default Implementation"))));
AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox);
AddStep("disable", () => target.Disable()); AddStep("disable", () => target.Disable());
AddAssert("consumer using base source", () => consumer.Drawable is BaseSourceBox); AddAssert("consumer using base source", () => consumer.Drawable is BaseSourceBox);

View File

@ -167,11 +167,16 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("start failing sends", () => AddStep("start failing sends", () =>
{ {
spectatorClient.ShouldFailSendingFrames = true; spectatorClient.ShouldFailSendingFrames = true;
framesReceivedSoFar = replay.Frames.Count;
frameSendAttemptsSoFar = spectatorClient.FrameSendAttempts; frameSendAttemptsSoFar = spectatorClient.FrameSendAttempts;
}); });
AddUntilStep("wait for send attempts", () => spectatorClient.FrameSendAttempts > frameSendAttemptsSoFar + 5); AddUntilStep("wait for next send attempt", () =>
{
framesReceivedSoFar = replay.Frames.Count;
return spectatorClient.FrameSendAttempts > frameSendAttemptsSoFar + 1;
});
AddUntilStep("wait for more send attempts", () => spectatorClient.FrameSendAttempts > frameSendAttemptsSoFar + 10);
AddAssert("frames did not increase", () => framesReceivedSoFar == replay.Frames.Count); AddAssert("frames did not increase", () => framesReceivedSoFar == replay.Frames.Count);
AddStep("stop failing sends", () => spectatorClient.ShouldFailSendingFrames = false); AddStep("stop failing sends", () => spectatorClient.ShouldFailSendingFrames = false);

View File

@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void createPlayerTest() private void createPlayerTest()
{ {
CreateTest(null); CreateTest();
AddAssert("storyboard loaded", () => Player.Beatmap.Value.Storyboard != null); AddAssert("storyboard loaded", () => Player.Beatmap.Value.Storyboard != null);
waitUntilStoryboardSamplesPlay(); waitUntilStoryboardSamplesPlay();

View File

@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay
base.SetUpSteps(); base.SetUpSteps();
AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true)); AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set dim level to 0", () => LocalConfig.SetValue<double>(OsuSetting.DimLevel, 0)); AddStep("set dim level to 0", () => LocalConfig.SetValue<double>(OsuSetting.DimLevel, 0));
AddStep("reset fail conditions", () => currentFailConditions = (_, __) => false); AddStep("reset fail conditions", () => currentFailConditions = (_, _) => false);
AddStep("set storyboard duration to 2s", () => currentStoryboardDuration = 2000); AddStep("set storyboard duration to 2s", () => currentStoryboardDuration = 2000);
AddStep("set ShowResults = true", () => showResults = true); AddStep("set ShowResults = true", () => showResults = true);
} }
@ -52,17 +52,18 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestStoryboardSkipOutro() public void TestStoryboardSkipOutro()
{ {
CreateTest(null); AddStep("set storyboard duration to long", () => currentStoryboardDuration = 200000);
CreateTest();
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space)); AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space));
AddAssert("player is no longer current screen", () => !Player.IsCurrentScreen()); AddUntilStep("player is no longer current screen", () => !Player.IsCurrentScreen());
AddUntilStep("wait for score shown", () => Player.IsScoreShown); AddUntilStep("wait for score shown", () => Player.IsScoreShown);
} }
[Test] [Test]
public void TestStoryboardNoSkipOutro() public void TestStoryboardNoSkipOutro()
{ {
CreateTest(null); CreateTest();
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for score shown", () => Player.IsScoreShown); AddUntilStep("wait for score shown", () => Player.IsScoreShown);
} }
@ -70,7 +71,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestStoryboardExitDuringOutroStillExits() public void TestStoryboardExitDuringOutroStillExits()
{ {
CreateTest(null); CreateTest();
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("exit via pause", () => Player.ExitViaPause()); AddStep("exit via pause", () => Player.ExitViaPause());
AddAssert("player exited", () => !Player.IsCurrentScreen() && Player.GetChildScreen() == null); AddAssert("player exited", () => !Player.IsCurrentScreen() && Player.GetChildScreen() == null);
@ -80,7 +81,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestCase(true)] [TestCase(true)]
public void TestStoryboardToggle(bool enabledAtBeginning) public void TestStoryboardToggle(bool enabledAtBeginning)
{ {
CreateTest(null); CreateTest();
AddStep($"{(enabledAtBeginning ? "enable" : "disable")} storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, enabledAtBeginning)); AddStep($"{(enabledAtBeginning ? "enable" : "disable")} storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, enabledAtBeginning));
AddStep("toggle storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, !enabledAtBeginning)); AddStep("toggle storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, !enabledAtBeginning));
AddUntilStep("wait for score shown", () => Player.IsScoreShown); AddUntilStep("wait for score shown", () => Player.IsScoreShown);
@ -91,7 +92,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
CreateTest(() => CreateTest(() =>
{ {
AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true); AddStep("fail on first judgement", () => currentFailConditions = (_, _) => true);
// Fail occurs at 164ms with the provided beatmap. // Fail occurs at 164ms with the provided beatmap.
// Fail animation runs for 2.5s realtime but the gameplay time change is *variable* due to the frequency transform being applied, so we need a bit of lenience. // Fail animation runs for 2.5s realtime but the gameplay time change is *variable* due to the frequency transform being applied, so we need a bit of lenience.
@ -129,7 +130,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
SkipOverlay.FadeContainer fadeContainer() => Player.ChildrenOfType<SkipOverlay.FadeContainer>().First(); SkipOverlay.FadeContainer fadeContainer() => Player.ChildrenOfType<SkipOverlay.FadeContainer>().First();
CreateTest(null); CreateTest();
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible); AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible);
@ -143,7 +144,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestPerformExitNoOutro() public void TestPerformExitNoOutro()
{ {
CreateTest(null); CreateTest();
AddStep("disable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, false)); AddStep("disable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, false));
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("exit via pause", () => Player.ExitViaPause()); AddStep("exit via pause", () => Player.ExitViaPause());

View File

@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
// To emulate `MultiplayerClient.CurrentMatchPlayingUserIds` we need a bindable list of *only IDs*. // To emulate `MultiplayerClient.CurrentMatchPlayingUserIds` we need a bindable list of *only IDs*.
// This tracks the list of users 1:1. // This tracks the list of users 1:1.
MultiplayerUsers.BindCollectionChanged((c, e) => MultiplayerUsers.BindCollectionChanged((_, e) =>
{ {
switch (e.Action) switch (e.Action)
{ {

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -139,6 +140,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for song select", () => (songSelect = CurrentSubScreen as Screens.Select.SongSelect) != null); AddUntilStep("wait for song select", () => (songSelect = CurrentSubScreen as Screens.Select.SongSelect) != null);
AddUntilStep("wait for loaded", () => songSelect.AsNonNull().BeatmapSetsLoaded); AddUntilStep("wait for loaded", () => songSelect.AsNonNull().BeatmapSetsLoaded);
AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value);
if (ruleset != null) if (ruleset != null)
AddStep($"set {ruleset.Name} ruleset", () => songSelect.AsNonNull().Ruleset.Value = ruleset); AddStep($"set {ruleset.Name} ruleset", () => songSelect.AsNonNull().Ruleset.Value = ruleset);

View File

@ -42,11 +42,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
var mockLounge = new Mock<LoungeSubScreen>(); var mockLounge = new Mock<LoungeSubScreen>();
mockLounge mockLounge
.Setup(l => l.Join(It.IsAny<Room>(), It.IsAny<string>(), It.IsAny<Action<Room>>(), It.IsAny<Action<string>>())) .Setup(l => l.Join(It.IsAny<Room>(), It.IsAny<string>(), It.IsAny<Action<Room>>(), It.IsAny<Action<string>>()))
.Callback<Room, string, Action<Room>, Action<string>>((a, b, c, d) => .Callback<Room, string, Action<Room>, Action<string>>((_, _, _, d) =>
{ {
Task.Run(() => Task.Run(() =>
{ {
allowResponseCallback.Wait(); allowResponseCallback.Wait(10000);
allowResponseCallback.Reset(); allowResponseCallback.Reset();
Schedule(() => d?.Invoke("Incorrect password")); Schedule(() => d?.Invoke("Incorrect password"));
}); });

View File

@ -104,6 +104,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
BeatmapInfo otherBeatmap = null; BeatmapInfo otherBeatmap = null;
AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value);
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap())); AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap()));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
@ -119,6 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value);
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(beatmap())); AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(beatmap()));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
} }

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