1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 11:37:28 +08:00

Merge branch 'master' into display-performance-attributes

This commit is contained in:
Henry Lin 2022-02-05 16:59:46 +08:00 committed by GitHub
commit f29301cd1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
362 changed files with 18809 additions and 2592 deletions

1
.gitignore vendored
View File

@ -339,3 +339,4 @@ inspectcode
# Fody (pulled in by Realm) - schema file # Fody (pulled in by Realm) - schema file
FodyWeavers.xsd FodyWeavers.xsd
**/FodyWeavers.xml

View File

@ -1,53 +1,75 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
CFPropertyList (3.0.3) CFPropertyList (3.0.5)
addressable (2.7.0) rexml
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
artifactory (3.0.15)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.1.0) aws-eventstream (1.2.0)
aws-partitions (1.413.0) aws-partitions (1.551.0)
aws-sdk-core (3.110.0) aws-sdk-core (3.125.5)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0) aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.40.0) aws-sdk-kms (1.53.0)
aws-sdk-core (~> 3, >= 3.109.0) aws-sdk-core (~> 3, >= 3.125.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.87.0) aws-sdk-s3 (1.111.3)
aws-sdk-core (~> 3, >= 3.109.0) aws-sdk-core (~> 3, >= 3.125.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.4)
aws-sigv4 (1.2.2) aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4) babosa (1.0.4)
claide (1.0.3) claide (1.1.0)
colored (1.2) colored (1.2)
colored2 (3.1.2) colored2 (3.1.2)
commander-fastlane (4.4.6) commander-fastlane (4.4.6)
highline (~> 1.7.2) highline (~> 1.7.2)
declarative (0.0.20) declarative (0.0.20)
declarative-option (0.1.0) digest-crc (0.6.4)
digest-crc (0.6.3)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701) domain_name (0.5.20190701)
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.1) emoji_regex (3.2.3)
excon (0.78.1) excon (0.90.0)
faraday (1.2.0) faraday (1.9.3)
multipart-post (>= 1.2, < 3) faraday-em_http (~> 1.0)
ruby2_keywords faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7) faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0) faraday (>= 0.8.0)
http-cookie (~> 1.0.0) http-cookie (~> 1.0.0)
faraday_middleware (1.0.0) faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.3)
multipart-post (>= 1.2, < 3)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.2.1) fastimage (2.2.6)
fastlane (2.170.0) fastlane (2.181.0)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0) addressable (>= 2.3, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0) aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0) babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0) bundler (>= 1.12.0, < 3.0.0)
@ -68,6 +90,7 @@ GEM
jwt (>= 2.1.0, < 3) jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0) mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0) multipart-post (~> 2.0.0)
naturally (~> 2.2)
plist (>= 3.1.0, < 4.0.0) plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0) rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3) security (= 0.1.3)
@ -94,65 +117,80 @@ GEM
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.0) retriable (>= 2.0, < 4.0)
signet (~> 0.12) signet (~> 0.12)
google-cloud-core (1.5.0) google-apis-core (0.4.2)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.10.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.11.0)
google-apis-core (>= 0.4, < 2.a)
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.4.0) google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.1) google-cloud-errors (1.2.0)
google-cloud-storage (1.29.2) google-cloud-storage (1.36.0)
addressable (~> 2.5) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-api-client (~> 0.33) google-apis-iamcredentials_v1 (~> 0.1)
google-cloud-core (~> 1.2) google-apis-storage_v1 (~> 0.1)
googleauth (~> 0.9) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (0.14.0) googleauth (0.17.1)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (~> 0.14) signet (~> 0.15)
highline (1.7.10) highline (1.7.10)
http-cookie (1.0.3) http-cookie (1.0.4)
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.8.3) httpclient (2.8.3)
jmespath (1.4.0) jmespath (1.5.0)
json (2.5.1) json (2.6.1)
jwt (2.2.2) jwt (2.3.0)
memoist (0.16.2) memoist (0.16.2)
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.0.2) mini_mime (1.1.2)
mini_portile2 (2.4.0) mini_portile2 (2.4.0)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.0.0) multipart-post (2.0.0)
nanaimo (0.3.0) nanaimo (0.3.0)
naturally (2.2.0) naturally (2.2.1)
nokogiri (1.10.10) nokogiri (1.10.10)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
os (1.1.1) os (1.1.4)
plist (3.5.0) plist (3.6.0)
public_suffix (4.0.6) public_suffix (4.0.6)
rake (13.0.3) rake (13.0.6)
representable (3.0.4) representable (3.1.1)
declarative (< 0.1.0) declarative (< 0.1.0)
declarative-option (< 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
retriable (3.1.2) retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7) rouge (2.0.7)
ruby2_keywords (0.0.2) ruby2_keywords (0.0.5)
rubyzip (2.3.0) rubyzip (2.3.2)
security (0.1.3) security (0.1.3)
signet (0.14.0) signet (0.16.0)
addressable (~> 2.3) addressable (~> 2.8)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
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)
CFPropertyList CFPropertyList
naturally naturally
slack-notifier (2.3.2) slack-notifier (2.4.0)
souyuz (0.9.1) souyuz (0.9.1)
fastlane (>= 1.103.0) fastlane (>= 1.103.0)
highline (~> 1.7) highline (~> 1.7)
@ -160,6 +198,7 @@ GEM
terminal-notifier (2.0.0) terminal-notifier (2.0.0)
terminal-table (1.8.0) terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.2)
tty-cursor (0.7.1) tty-cursor (0.7.1)
tty-screen (0.8.1) tty-screen (0.8.1)
tty-spinner (0.9.3) tty-spinner (0.9.3)
@ -167,18 +206,20 @@ 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.7.7) unf_ext (0.0.8)
unicode-display_width (1.7.0) unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.19.0) xcodeproj (1.21.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)
colored2 (~> 3.1) colored2 (~> 3.1)
nanaimo (~> 0.3.0) nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0) xcpretty (0.3.0)
rouge (~> 2.0.7) rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0) xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7) xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS PLATFORMS

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Tests
[STAThread] [STAThread]
public static int Main(string[] args) public static int Main(string[] args)
{ {
using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true }))
{ {
host.Run(new OsuTestBrowser()); host.Run(new OsuTestBrowser());
return 0; return 0;

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Replays
protected override bool IsImportant(EmptyFreeformReplayFrame frame) => frame.Actions.Any(); protected override bool IsImportant(EmptyFreeformReplayFrame frame) => frame.Actions.Any();
public override void CollectPendingInputs(List<IInput> inputs) protected override void CollectReplayInputs(List<IInput> inputs)
{ {
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time); var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests
[STAThread] [STAThread]
public static int Main(string[] args) public static int Main(string[] args)
{ {
using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true }))
{ {
host.Run(new OsuTestBrowser()); host.Run(new OsuTestBrowser());
return 0; return 0;

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Pippidon.Replays
protected override bool IsImportant(PippidonReplayFrame frame) => true; protected override bool IsImportant(PippidonReplayFrame frame) => true;
public override void CollectPendingInputs(List<IInput> inputs) protected override void CollectReplayInputs(List<IInput> inputs)
{ {
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time); var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Tests
[STAThread] [STAThread]
public static int Main(string[] args) public static int Main(string[] args)
{ {
using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true }))
{ {
host.Run(new OsuTestBrowser()); host.Run(new OsuTestBrowser());
return 0; return 0;

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Replays
protected override bool IsImportant(EmptyScrollingReplayFrame frame) => frame.Actions.Any(); protected override bool IsImportant(EmptyScrollingReplayFrame frame) => frame.Actions.Any();
public override void CollectPendingInputs(List<IInput> inputs) protected override void CollectReplayInputs(List<IInput> inputs)
{ {
inputs.Add(new ReplayState<EmptyScrollingAction> inputs.Add(new ReplayState<EmptyScrollingAction>
{ {

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests
[STAThread] [STAThread]
public static int Main(string[] args) public static int Main(string[] args)
{ {
using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true }))
{ {
host.Run(new OsuTestBrowser()); host.Run(new OsuTestBrowser());
return 0; return 0;

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Pippidon.Replays
protected override bool IsImportant(PippidonReplayFrame frame) => frame.Actions.Any(); protected override bool IsImportant(PippidonReplayFrame frame) => frame.Actions.Any();
public override void CollectPendingInputs(List<IInput> inputs) protected override void CollectReplayInputs(List<IInput> inputs)
{ {
inputs.Add(new ReplayState<PippidonAction> inputs.Add(new ReplayState<PippidonAction>
{ {

View File

@ -12,7 +12,7 @@ Install _fastlane_ using
``` ```
[sudo] gem install fastlane -NV [sudo] gem install fastlane -NV
``` ```
or alternatively using `brew cask install fastlane` or alternatively using `brew install fastlane`
# Available Actions # Available Actions
## Android ## Android

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.115.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.204.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.118.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.204.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

@ -27,7 +27,7 @@ namespace osu.Android
{ {
gameActivity.RunOnUiThread(() => gameActivity.RunOnUiThread(() =>
{ {
gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser; gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
}); });
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -8,16 +9,18 @@ using System.Threading.Tasks;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Android.Net; using Android.Graphics;
using Android.OS; using Android.OS;
using Android.Provider; using Android.Provider;
using Android.Views; using Android.Views;
using osu.Framework.Android; using osu.Framework.Android;
using osu.Game.Database; using osu.Game.Database;
using Debug = System.Diagnostics.Debug;
using Uri = Android.Net.Uri;
namespace osu.Android namespace osu.Android
{ {
[Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser)] [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")]
@ -41,6 +44,12 @@ namespace osu.Android
{ {
private static readonly string[] osu_url_schemes = { "osu", "osump" }; private static readonly string[] osu_url_schemes = { "osu", "osump" };
/// <summary>
/// The default screen orientation.
/// </summary>
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
private OsuGameAndroid game; private OsuGameAndroid game;
protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this);
@ -54,8 +63,20 @@ namespace osu.Android
// reference: https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent) // reference: https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent)
handleIntent(Intent); handleIntent(Intent);
Debug.Assert(Window != null);
Window.AddFlags(WindowManagerFlags.Fullscreen); Window.AddFlags(WindowManagerFlags.Fullscreen);
Window.AddFlags(WindowManagerFlags.KeepScreenOn); Window.AddFlags(WindowManagerFlags.KeepScreenOn);
Debug.Assert(WindowManager?.DefaultDisplay != null);
Debug.Assert(Resources?.DisplayMetrics != null);
Point displaySize = new Point();
WindowManager.DefaultDisplay.GetSize(displaySize);
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
bool isTablet = smallestWidthDp >= 600f;
RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
} }
protected override void OnNewIntent(Intent intent) => handleIntent(intent); protected override void OnNewIntent(Intent intent) => handleIntent(intent);
@ -104,7 +125,7 @@ namespace osu.Android
cursor.MoveToFirst(); cursor.MoveToFirst();
var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName); int filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName);
string filename = cursor.GetString(filenameColumn); string filename = cursor.GetString(filenameColumn);
// SharpCompress requires archive streams to be seekable, which the stream opened by // SharpCompress requires archive streams to be seekable, which the stream opened by

View File

@ -55,7 +55,7 @@ namespace osu.Desktop
} }
} }
using (DesktopGameHost host = Host.GetSuitableHost(gameName, true)) using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true }))
{ {
host.ExceptionThrown += handleException; host.ExceptionThrown += handleException;

View File

@ -16,7 +16,7 @@ namespace osu.Game.Benchmarks
public class BenchmarkRealmReads : BenchmarkTest public class BenchmarkRealmReads : BenchmarkTest
{ {
private TemporaryNativeStorage storage; private TemporaryNativeStorage storage;
private RealmContextFactory realmFactory; private RealmAccess realm;
private UpdateThread updateThread; private UpdateThread updateThread;
[Params(1, 100, 1000)] [Params(1, 100, 1000)]
@ -27,9 +27,9 @@ namespace osu.Game.Benchmarks
storage = new TemporaryNativeStorage("realm-benchmark"); storage = new TemporaryNativeStorage("realm-benchmark");
storage.DeleteDirectory(string.Empty); storage.DeleteDirectory(string.Empty);
realmFactory = new RealmContextFactory(storage, "client"); realm = new RealmAccess(storage, "client");
realmFactory.Run(realm => realm.Run(r =>
{ {
realm.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo }))); realm.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo })));
}); });
@ -41,9 +41,9 @@ namespace osu.Game.Benchmarks
[Benchmark] [Benchmark]
public void BenchmarkDirectPropertyRead() public void BenchmarkDirectPropertyRead()
{ {
realmFactory.Run(realm => realm.Run(r =>
{ {
var beatmapSet = realm.All<BeatmapSetInfo>().First(); var beatmapSet = r.All<BeatmapSetInfo>().First();
for (int i = 0; i < ReadsPerFetch; i++) for (int i = 0; i < ReadsPerFetch; i++)
{ {
@ -61,7 +61,7 @@ namespace osu.Game.Benchmarks
{ {
try try
{ {
var beatmapSet = realmFactory.Context.All<BeatmapSetInfo>().First(); var beatmapSet = realm.Realm.All<BeatmapSetInfo>().First();
for (int i = 0; i < ReadsPerFetch; i++) for (int i = 0; i < ReadsPerFetch; i++)
{ {
@ -80,9 +80,9 @@ namespace osu.Game.Benchmarks
[Benchmark] [Benchmark]
public void BenchmarkRealmLivePropertyRead() public void BenchmarkRealmLivePropertyRead()
{ {
realmFactory.Run(realm => realm.Run(r =>
{ {
var beatmapSet = realm.All<BeatmapSetInfo>().First().ToLive(realmFactory); var beatmapSet = r.All<BeatmapSetInfo>().First().ToLive(realm);
for (int i = 0; i < ReadsPerFetch; i++) for (int i = 0; i < ReadsPerFetch; i++)
{ {
@ -100,7 +100,7 @@ namespace osu.Game.Benchmarks
{ {
try try
{ {
var beatmapSet = realmFactory.Context.All<BeatmapSetInfo>().First().ToLive(realmFactory); var beatmapSet = realm.Realm.All<BeatmapSetInfo>().First().ToLive(realm);
for (int i = 0; i < ReadsPerFetch; i++) for (int i = 0; i < ReadsPerFetch; i++)
{ {
@ -119,9 +119,9 @@ namespace osu.Game.Benchmarks
[Benchmark] [Benchmark]
public void BenchmarkDetachedPropertyRead() public void BenchmarkDetachedPropertyRead()
{ {
realmFactory.Run(realm => realm.Run(r =>
{ {
var beatmapSet = realm.All<BeatmapSetInfo>().First().Detach(); var beatmapSet = r.All<BeatmapSetInfo>().First().Detach();
for (int i = 0; i < ReadsPerFetch; i++) for (int i = 0; i < ReadsPerFetch; i++)
{ {
@ -133,7 +133,7 @@ namespace osu.Game.Benchmarks
[GlobalCleanup] [GlobalCleanup]
public void Cleanup() public void Cleanup()
{ {
realmFactory?.Dispose(); realm?.Dispose();
storage?.Dispose(); storage?.Dispose();
updateThread?.Exit(); updateThread?.Exit();
} }

View File

@ -24,11 +24,16 @@
<string>armv7</string> <string>armv7</string>
</array> </array>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array> </array>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string> <string>Assets.xcassets/AppIcon.appiconset</string>

View File

@ -3,6 +3,7 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -15,9 +16,26 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => 1.12;
private const float default_flashlight_size = 350; [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
{
MinValue = 0.5f,
MaxValue = 1.5f,
Default = 1f,
Value = 1f,
Precision = 0.1f
};
public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield); [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
public override BindableBool ComboBasedSize { get; } = new BindableBool
{
Default = true,
Value = true
};
public override float DefaultFlashlightSize => 350;
protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield);
private CatchPlayfield playfield; private CatchPlayfield playfield;
@ -31,10 +49,11 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
private readonly CatchPlayfield playfield; private readonly CatchPlayfield playfield;
public CatchFlashlight(CatchPlayfield playfield) public CatchFlashlight(CatchModFlashlight modFlashlight, CatchPlayfield playfield)
: base(modFlashlight)
{ {
this.playfield = playfield; this.playfield = playfield;
FlashlightSize = new Vector2(0, getSizeFor(0)); FlashlightSize = new Vector2(0, GetSizeFor(0));
} }
protected override void Update() protected override void Update()
@ -44,19 +63,9 @@ namespace osu.Game.Rulesets.Catch.Mods
FlashlightPosition = playfield.CatcherArea.ToSpaceOfOtherDrawable(playfield.Catcher.DrawPosition, this); FlashlightPosition = playfield.CatcherArea.ToSpaceOfOtherDrawable(playfield.Catcher.DrawPosition, this);
} }
private float getSizeFor(int combo)
{
if (combo > 200)
return default_flashlight_size * 0.8f;
else if (combo > 100)
return default_flashlight_size * 0.9f;
else
return default_flashlight_size;
}
protected override void OnComboChange(ValueChangedEvent<int> e) protected override void OnComboChange(ValueChangedEvent<int> e)
{ {
this.TransformTo(nameof(FlashlightSize), new Vector2(0, getSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION);
} }
protected override string FragmentShader => "CircularFlashlight"; protected override string FragmentShader => "CircularFlashlight";

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Replays
protected override bool IsImportant(CatchReplayFrame frame) => frame.Actions.Any(); protected override bool IsImportant(CatchReplayFrame frame) => frame.Actions.Any();
public override void CollectPendingInputs(List<IInput> inputs) protected override void CollectReplayInputs(List<IInput> inputs)
{ {
float position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time); float position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);

View File

@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
case CatchSkinComponents.CatchComboCounter: case CatchSkinComponents.CatchComboCounter:
if (providesComboCounter) if (providesComboCounter)
return new LegacyCatchComboCounter(Skin); return new LegacyCatchComboCounter();
return null; return null;

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
private readonly LegacyRollingCounter explosion; private readonly LegacyRollingCounter explosion;
public LegacyCatchComboCounter(ISkin skin) public LegacyCatchComboCounter()
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;

View File

@ -24,11 +24,16 @@
<string>armv7</string> <string>armv7</string>
</array> </array>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array> </array>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string> <string>Assets.xcassets/AppIcon.appiconset</string>

View File

@ -0,0 +1,103 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
using System.Collections.Generic;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public class TestSceneManiaModHoldOff : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Test]
public void TestMapHasNoHoldNotes()
{
var testBeatmap = createModdedBeatmap();
Assert.False(testBeatmap.HitObjects.OfType<HoldNote>().Any());
}
[Test]
public void TestCorrectNoteValues()
{
var testBeatmap = createRawBeatmap();
var noteValues = new List<double>(testBeatmap.HitObjects.OfType<HoldNote>().Count());
foreach (HoldNote h in testBeatmap.HitObjects.OfType<HoldNote>())
{
noteValues.Add(ManiaModHoldOff.GetNoteDurationInBeatLength(h, testBeatmap));
}
noteValues.Sort();
Assert.AreEqual(noteValues, new List<double> { 0.125, 0.250, 0.500, 1.000, 2.000 });
}
[Test]
public void TestCorrectObjectCount()
{
// Ensure that the mod produces the expected number of objects when applied.
var rawBeatmap = createRawBeatmap();
var testBeatmap = createModdedBeatmap();
// Calculate expected number of objects
int expectedObjectCount = 0;
foreach (ManiaHitObject h in rawBeatmap.HitObjects)
{
// Both notes and hold notes account for at least one object
expectedObjectCount++;
if (h.GetType() == typeof(HoldNote))
{
double noteValue = ManiaModHoldOff.GetNoteDurationInBeatLength((HoldNote)h, rawBeatmap);
if (noteValue >= ManiaModHoldOff.END_NOTE_ALLOW_THRESHOLD)
{
// Should generate an end note if it's longer than the minimum note value
expectedObjectCount++;
}
}
}
Assert.That(testBeatmap.HitObjects.Count == expectedObjectCount);
}
private static ManiaBeatmap createModdedBeatmap()
{
var beatmap = createRawBeatmap();
var holdOffMod = new ManiaModHoldOff();
foreach (var hitObject in beatmap.HitObjects)
hitObject.ApplyDefaults(beatmap.ControlPointInfo, new BeatmapDifficulty());
holdOffMod.ApplyToBeatmap(beatmap);
return beatmap;
}
private static ManiaBeatmap createRawBeatmap()
{
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 });
beatmap.ControlPointInfo.Add(0.0, new TimingControlPoint { BeatLength = 1000 }); // Set BPM to 60
// Add test hit objects
beatmap.HitObjects.Add(new Note { StartTime = 4000 });
beatmap.HitObjects.Add(new Note { StartTime = 4500 });
beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 125 }); // 1/8 note
beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 250 }); // 1/4 note
beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 500 }); // 1/2 note
beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 1000 }); // 1/1 note
beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 2000 }); // 2/1 note
return beatmap;
}
}
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Width = 0.5f, Width = 0.5f,
Child = new ColumnHitObjectArea(0, new HitObjectContainer()) Child = new ColumnHitObjectArea(new HitObjectContainer())
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
} }
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Width = 0.5f, Width = 0.5f,
Child = new ColumnHitObjectArea(1, new HitObjectContainer()) Child = new ColumnHitObjectArea(new HitObjectContainer())
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
} }

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania
public bool Matches(BeatmapInfo beatmapInfo) public bool Matches(BeatmapInfo beatmapInfo)
{ {
return !keys.HasFilter || (beatmapInfo.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo))); return !keys.HasFilter || (beatmapInfo.Ruleset.OnlineID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo)));
} }
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)

View File

@ -2,11 +2,13 @@
// 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.ComponentModel; using System.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania namespace osu.Game.Rulesets.Mania
{ {
[Cached] // Used for touch input, see ColumnTouchInputArea.
public class ManiaInputManager : RulesetInputManager<ManiaAction> public class ManiaInputManager : RulesetInputManager<ManiaAction>
{ {
public ManiaInputManager(RulesetInfo ruleset, int variant) public ManiaInputManager(RulesetInfo ruleset, int variant)

View File

@ -243,7 +243,8 @@ namespace osu.Game.Rulesets.Mania
new ManiaModDifficultyAdjust(), new ManiaModDifficultyAdjust(),
new ManiaModClassic(), new ManiaModClassic(),
new ManiaModInvert(), new ManiaModInvert(),
new ManiaModConstantSpeed() new ManiaModConstantSpeed(),
new ManiaModHoldOff()
}; };
case ModType.Automation: case ModType.Automation:
@ -369,21 +370,21 @@ namespace osu.Game.Rulesets.Mania
{ {
Columns = new[] Columns = new[]
{ {
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents) new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 250 Height = 250
}), }, true),
} }
}, },
new StatisticRow new StatisticRow
{ {
Columns = new[] Columns = new[]
{ {
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{ {
new UnstableRate(score.HitEvents) new UnstableRate(score.HitEvents)
})) }), true)
} }
} }
}; };

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osuTK; using osuTK;
@ -16,17 +17,35 @@ namespace osu.Game.Rulesets.Mania.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModHidden) }; public override Type[] IncompatibleMods => new[] { typeof(ModHidden) };
private const float default_flashlight_size = 180; [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
{
MinValue = 0.5f,
MaxValue = 3f,
Default = 1f,
Value = 1f,
Precision = 0.1f
};
public override Flashlight CreateFlashlight() => new ManiaFlashlight(); [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
public override BindableBool ComboBasedSize { get; } = new BindableBool
{
Default = false,
Value = false
};
public override float DefaultFlashlightSize => 50;
protected override Flashlight CreateFlashlight() => new ManiaFlashlight(this);
private class ManiaFlashlight : Flashlight private class ManiaFlashlight : Flashlight
{ {
private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize); private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize);
public ManiaFlashlight() public ManiaFlashlight(ManiaModFlashlight modFlashlight)
: base(modFlashlight)
{ {
FlashlightSize = new Vector2(0, default_flashlight_size); FlashlightSize = new Vector2(DrawWidth, GetSizeFor(0));
AddLayout(flashlightProperties); AddLayout(flashlightProperties);
} }
@ -46,6 +65,7 @@ namespace osu.Game.Rulesets.Mania.Mods
protected override void OnComboChange(ValueChangedEvent<int> e) protected override void OnComboChange(ValueChangedEvent<int> e)
{ {
this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION);
} }
protected override string FragmentShader => "RectangularFlashlight"; protected override string FragmentShader => "RectangularFlashlight";

View File

@ -0,0 +1,72 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
using osu.Game.Rulesets.Mania.Beatmaps;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModHoldOff : Mod, IApplicableAfterBeatmapConversion
{
public override string Name => "Hold Off";
public override string Acronym => "HO";
public override double ScoreMultiplier => 1;
public override string Description => @"Replaces all hold notes with normal notes.";
public override IconUsage? Icon => FontAwesome.Solid.DotCircle;
public override ModType Type => ModType.Conversion;
public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) };
public const double END_NOTE_ALLOW_THRESHOLD = 0.5;
public void ApplyToBeatmap(IBeatmap beatmap)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
var newObjects = new List<ManiaHitObject>();
foreach (var h in beatmap.HitObjects.OfType<HoldNote>())
{
// Add a note for the beginning of the hold note
newObjects.Add(new Note
{
Column = h.Column,
StartTime = h.StartTime,
Samples = h.GetNodeSamples(0)
});
// Don't add an end note if the duration is shorter than the threshold
double noteValue = GetNoteDurationInBeatLength(h, maniaBeatmap); // 1/1, 1/2, 1/4, etc.
if (noteValue >= END_NOTE_ALLOW_THRESHOLD)
{
newObjects.Add(new Note
{
Column = h.Column,
StartTime = h.EndTime,
Samples = h.GetNodeSamples((h.NodeSamples?.Count - 1) ?? 1)
});
}
}
maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType<Note>().Concat(newObjects).OrderBy(h => h.StartTime).ToList();
}
public static double GetNoteDurationInBeatLength(HoldNote holdNote, ManiaBeatmap beatmap)
{
double beatLength = beatmap.ControlPointInfo.TimingPointAt(holdNote.StartTime).BeatLength;
return holdNote.Duration / beatLength;
}
}
}

View File

@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Mania.Mods
public override ModType Type => ModType.Conversion; public override ModType Type => ModType.Conversion;
public override Type[] IncompatibleMods => new[] { typeof(ManiaModHoldOff) };
public void ApplyToBeatmap(IBeatmap beatmap) public void ApplyToBeatmap(IBeatmap beatmap)
{ {
var maniaBeatmap = (ManiaBeatmap)beatmap; var maniaBeatmap = (ManiaBeatmap)beatmap;

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Replays
protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any(); protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any();
public override void CollectPendingInputs(List<IInput> inputs) protected override void CollectReplayInputs(List<IInput> inputs)
{ {
inputs.Add(new ReplayState<ManiaAction> { PressedActions = CurrentFrame?.Actions ?? new List<ManiaAction>() }); inputs.Add(new ReplayState<ManiaAction> { PressedActions = CurrentFrame?.Actions ?? new List<ManiaAction>() });
} }

View File

@ -62,13 +62,14 @@ namespace osu.Game.Rulesets.Mania.UI
sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer), sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer),
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(), background.CreateProxy(),
HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both }, HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both },
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
background, background,
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both },
new ColumnTouchInputArea(this)
}; };
hitPolicy = new OrderedHitPolicy(HitObjectContainer); hitPolicy = new OrderedHitPolicy(HitObjectContainer);
@ -139,5 +140,50 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
public class ColumnTouchInputArea : Drawable
{
private readonly Column column;
[Resolved(canBeNull: true)]
private ManiaInputManager maniaInputManager { get; set; }
private KeyBindingContainer<ManiaAction> keyBindingContainer;
public ColumnTouchInputArea(Column column)
{
RelativeSizeAxes = Axes.Both;
this.column = column;
}
protected override void LoadComplete()
{
keyBindingContainer = maniaInputManager?.KeyBindingContainer;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
keyBindingContainer?.TriggerPressed(column.Action.Value);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
keyBindingContainer?.TriggerReleased(column.Action.Value);
base.OnMouseUp(e);
}
protected override bool OnTouchDown(TouchDownEvent e)
{
keyBindingContainer?.TriggerPressed(column.Action.Value);
return true;
}
protected override void OnTouchUp(TouchUpEvent e)
{
keyBindingContainer?.TriggerReleased(column.Action.Value);
}
}
} }
} }

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
private readonly Drawable hitTarget; private readonly Drawable hitTarget;
public ColumnHitObjectArea(int columnIndex, HitObjectContainer hitObjectContainer) public ColumnHitObjectArea(HitObjectContainer hitObjectContainer)
: base(hitObjectContainer) : base(hitObjectContainer)
{ {
AddRangeInternal(new[] AddRangeInternal(new[]

View File

@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
AlwaysPresent = true AlwaysPresent = true
} }
} }
} },
} }
}; };

View File

@ -24,11 +24,16 @@
<string>armv7</string> <string>armv7</string>
</array> </array>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array> </array>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string> <string>Assets.xcassets/AppIcon.appiconset</string>

View File

@ -0,0 +1,98 @@
// 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.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSliderVelocityAdjust : OsuGameTestScene
{
private Screens.Edit.Editor editor => Game.ScreenStack.CurrentScreen as Screens.Edit.Editor;
private EditorBeatmap editorBeatmap => editor.ChildrenOfType<EditorBeatmap>().FirstOrDefault();
private EditorClock editorClock => editor.ChildrenOfType<EditorClock>().FirstOrDefault();
private Slider slider => editorBeatmap.HitObjects.OfType<Slider>().FirstOrDefault();
private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType<TimelineHitObjectBlueprint>().FirstOrDefault();
private DifficultyPointPiece difficultyPointPiece => blueprint.ChildrenOfType<DifficultyPointPiece>().First();
private IndeterminateSliderWithTextBoxInput<double> velocityTextBox => Game.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().First().ChildrenOfType<IndeterminateSliderWithTextBoxInput<double>>().First();
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(false)]
public void TestVelocityChangeSavesCorrectly(bool adjustVelocity)
{
double? velocity = null;
AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader()));
AddUntilStep("wait for editor load", () => editorComponentsReady);
AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time));
AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3));
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(editor.ChildrenOfType<Playfield>().First().ScreenSpaceDrawQuad.Centre));
AddStep("start placement", () => InputManager.Click(MouseButton.Left));
AddStep("move mouse to bottom right", () => InputManager.MoveMouseTo(editor.ChildrenOfType<Playfield>().First().ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
AddStep("end placement", () => InputManager.Click(MouseButton.Right));
AddStep("exit placement mode", () => InputManager.Key(Key.Number1));
AddAssert("slider placed", () => slider != null);
AddStep("select slider", () => editorBeatmap.SelectedHitObjects.Add(slider));
AddAssert("ensure one slider placed", () => slider != null);
AddStep("store velocity", () => velocity = slider.Velocity);
if (adjustVelocity)
{
AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick());
AddStep("change velocity", () => velocityTextBox.Current.Value = 2);
AddAssert("velocity adjusted", () =>
{
Debug.Assert(velocity != null);
return Precision.AlmostEquals(velocity.Value * 2, slider.Velocity);
});
AddStep("store velocity", () => velocity = slider.Velocity);
}
AddStep("save", () => InputManager.Keys(PlatformAction.Save));
AddStep("exit", () => InputManager.Key(Key.Escape));
AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader()));
AddUntilStep("wait for editor load", () => editorComponentsReady);
AddStep("seek to slider", () => editorClock.Seek(slider.StartTime));
AddAssert("slider has correct velocity", () => slider.Velocity == velocity);
}
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModAimAssist : OsuModTestScene
{
[TestCase(0.1f)]
[TestCase(0.5f)]
[TestCase(1)]
public void TestAimAssist(float strength)
{
CreateModTest(new ModTestData
{
Mod = new OsuModAimAssist
{
AssistStrength = { Value = strength },
},
PassCondition = () => true,
Autoplay = false,
});
}
}
}

View File

@ -0,0 +1,154 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModAlternate : OsuModTestScene
{
[Test]
public void TestInputAtIntro() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(200)),
new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton),
}
});
[Test]
public void TestInputAlternating() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 4,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 1000,
Position = new Vector2(200, 100),
},
new HitCircle
{
StartTime = 1500,
Position = new Vector2(300, 100),
},
new HitCircle
{
StartTime = 2000,
Position = new Vector2(400, 100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.RightButton),
new OsuReplayFrame(1001, new Vector2(200, 100)),
new OsuReplayFrame(1500, new Vector2(300, 100), OsuAction.LeftButton),
new OsuReplayFrame(1501, new Vector2(300, 100)),
new OsuReplayFrame(2000, new Vector2(400, 100), OsuAction.RightButton),
new OsuReplayFrame(2001, new Vector2(400, 100)),
}
});
[Test]
public void TestInputSingular() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 1000,
Position = new Vector2(200, 100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.LeftButton),
}
});
[Test]
public void TestInputSingularWithBreak() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
Autoplay = false,
Beatmap = new Beatmap
{
Breaks = new List<BreakPeriod>
{
new BreakPeriod(500, 2250),
},
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 2500,
Position = new Vector2(100),
}
}
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(2500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(2501, new Vector2(100)),
}
});
}
}

View File

@ -145,6 +145,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
private bool isBreak() => Player.IsBreakTime.Value; private bool isBreak() => Player.IsBreakTime.Value;
private bool cursorAlphaAlmostEquals(float alpha) => Precision.AlmostEquals(Player.DrawableRuleset.Cursor.Alpha, alpha); private bool cursorAlphaAlmostEquals(float alpha) => Precision.AlmostEquals(Player.DrawableRuleset.Cursor.Alpha, alpha, 0.1f);
} }
} }

View File

@ -118,7 +118,6 @@ namespace osu.Game.Rulesets.Osu.Tests
public Drawable GetDrawableComponent(ISkinComponent component) => null; public Drawable GetDrawableComponent(ISkinComponent component) => null;
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
public ISample GetSample(ISampleInfo sampleInfo) => null; public ISample GetSample(ISampleInfo sampleInfo) => null;
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{ {

View File

@ -0,0 +1,83 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
internal class OsuModAimAssist : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Aim Assist";
public override string Acronym => "AA";
public override IconUsage? Icon => FontAwesome.Solid.MousePointer;
public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circle the circle chases you!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) };
private IFrameStableClock gameplayClock;
[SettingSource("Assist strength", "How much this mod will assist you.", 0)]
public BindableFloat AssistStrength { get; } = new BindableFloat(0.5f)
{
Precision = 0.05f,
MinValue = 0.05f,
MaxValue = 1.0f,
};
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
gameplayClock = drawableRuleset.FrameStableClock;
// Hide judgment displays and follow points as they won't make any sense.
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
drawableRuleset.Playfield.DisplayJudgements.Value = false;
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
}
public void Update(Playfield playfield)
{
var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{
switch (drawable)
{
case DrawableHitCircle circle:
easeTo(circle, cursorPos);
break;
case DrawableSlider slider:
if (!slider.HeadCircle.Result.HasResult)
easeTo(slider, cursorPos);
else
easeTo(slider, cursorPos - slider.Ball.DrawPosition);
break;
}
}
}
private void easeTo(DrawableHitObject hitObject, Vector2 destination)
{
double dampLength = Interpolation.Lerp(3000, 40, AssistStrength.Value);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);
hitObject.Position = new Vector2(x, y);
}
}
}

View File

@ -0,0 +1,106 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{
public override string Name => @"Alternate";
public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!";
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) };
public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
private double firstObjectValidJudgementTime;
private IBindable<bool> isBreakTime;
private const double flash_duration = 1000;
private OsuAction? lastActionPressed;
private DrawableRuleset<OsuHitObject> ruleset;
private IFrameStableClock gameplayClock;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var firstHitObject = ruleset.Objects.FirstOrDefault();
firstObjectValidJudgementTime = (firstHitObject?.StartTime ?? 0) - (firstHitObject?.HitWindows.WindowFor(HitResult.Meh) ?? 0);
gameplayClock = drawableRuleset.FrameStableClock;
}
public void ApplyToPlayer(Player player)
{
isBreakTime = player.IsBreakTime.GetBoundCopy();
isBreakTime.ValueChanged += e =>
{
if (e.NewValue)
lastActionPressed = null;
};
}
private bool checkCorrectAction(OsuAction action)
{
if (isBreakTime.Value)
return true;
if (gameplayClock.CurrentTime < firstObjectValidJudgementTime)
return true;
switch (action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
break;
// Any action which is not left or right button should be ignored.
default:
return true;
}
if (lastActionPressed != action)
{
// User alternated correctly.
lastActionPressed = action;
return true;
}
ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
return false;
}
private class InputInterceptor : Component, IKeyBindingHandler<OsuAction>
{
private readonly OsuModAlternate mod;
public InputInterceptor(OsuModAlternate mod)
{
this.mod = mod;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
// if the pressed action is incorrect, block it from reaching gameplay.
=> !mod.checkCorrectAction(e.Action);
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
}
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation; public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm."; public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModAimAssist) };
public bool PerformFail() => false; public bool PerformFail() => false;

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAutoplay : ModAutoplay public class OsuModAutoplay : ModAutoplay
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModCinema : ModCinema<OsuHitObject> public class OsuModCinema : ModCinema<OsuHitObject>
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -12,7 +12,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
@ -21,27 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => 1.12;
private const float default_flashlight_size = 180;
private const double default_follow_delay = 120; private const double default_follow_delay = 120;
private OsuFlashlight flashlight;
public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight();
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
if (drawable is DrawableSlider s)
s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange;
}
public override void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
base.ApplyToDrawableRuleset(drawableRuleset);
flashlight.FollowDelay = FollowDelay.Value;
}
[SettingSource("Follow delay", "Milliseconds until the flashlight reaches the cursor")] [SettingSource("Follow delay", "Milliseconds until the flashlight reaches the cursor")]
public BindableNumber<double> FollowDelay { get; } = new BindableDouble(default_follow_delay) public BindableNumber<double> FollowDelay { get; } = new BindableDouble(default_follow_delay)
{ {
@ -50,13 +30,45 @@ namespace osu.Game.Rulesets.Osu.Mods
Precision = default_follow_delay, Precision = default_follow_delay,
}; };
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
{
MinValue = 0.5f,
MaxValue = 2f,
Default = 1f,
Value = 1f,
Precision = 0.1f
};
[SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
public override BindableBool ComboBasedSize { get; } = new BindableBool
{
Default = true,
Value = true
};
public override float DefaultFlashlightSize => 180;
private OsuFlashlight flashlight;
protected override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(this);
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
if (drawable is DrawableSlider s)
s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange;
}
private class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition private class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition
{ {
public double FollowDelay { private get; set; } private readonly double followDelay;
public OsuFlashlight() public OsuFlashlight(OsuModFlashlight modFlashlight)
: base(modFlashlight)
{ {
FlashlightSize = new Vector2(0, getSizeFor(0)); followDelay = modFlashlight.FollowDelay.Value;
FlashlightSize = new Vector2(0, GetSizeFor(0));
} }
public void OnSliderTrackingChange(ValueChangedEvent<bool> e) public void OnSliderTrackingChange(ValueChangedEvent<bool> e)
@ -71,24 +83,14 @@ namespace osu.Game.Rulesets.Osu.Mods
var destination = e.MousePosition; var destination = e.MousePosition;
FlashlightPosition = Interpolation.ValueAt( FlashlightPosition = Interpolation.ValueAt(
Math.Min(Math.Abs(Clock.ElapsedFrameTime), FollowDelay), position, destination, 0, FollowDelay, Easing.Out); Math.Min(Math.Abs(Clock.ElapsedFrameTime), followDelay), position, destination, 0, followDelay, Easing.Out);
return base.OnMouseMove(e); return base.OnMouseMove(e);
} }
private float getSizeFor(int combo)
{
if (combo > 200)
return default_flashlight_size * 0.8f;
else if (combo > 100)
return default_flashlight_size * 0.9f;
else
return default_flashlight_size;
}
protected override void OnComboChange(ValueChangedEvent<int> e) protected override void OnComboChange(ValueChangedEvent<int> e)
{ {
this.TransformTo(nameof(FlashlightSize), new Vector2(0, getSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION);
} }
protected override string FragmentShader => "CircularFlashlight"; protected override string FragmentShader => "CircularFlashlight";

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING."; public override string Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModAimAssist) };
private float theta; private float theta;

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "They just won't stay still..."; public override string Description => "They just won't stay still...";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModAimAssist) };
private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles
private const int wiggle_strength = 10; // Higher = stronger wiggles private const int wiggle_strength = 10; // Higher = stronger wiggles

View File

@ -169,6 +169,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModClassic(), new OsuModClassic(),
new OsuModRandom(), new OsuModRandom(),
new OsuModMirror(), new OsuModMirror(),
new OsuModAlternate(),
}; };
case ModType.Automation: case ModType.Automation:
@ -193,6 +194,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModApproachDifferent(), new OsuModApproachDifferent(),
new OsuModMuted(), new OsuModMuted(),
new OsuModNoScope(), new OsuModNoScope(),
new OsuModAimAssist(),
}; };
case ModType.System: case ModType.System:
@ -277,33 +279,32 @@ namespace osu.Game.Rulesets.Osu
{ {
Columns = new[] Columns = new[]
{ {
new StatisticItem("Timing Distribution", new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
new HitEventTimingDistributionGraph(timedHitEvents)
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 250 Height = 250
}), }, true),
} }
}, },
new StatisticRow new StatisticRow
{ {
Columns = new[] Columns = new[]
{ {
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] new StatisticItem("Accuracy Heatmap", () => new AccuracyHeatmap(score, playableBeatmap)
{
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{ {
new UnstableRate(timedHitEvents) new UnstableRate(timedHitEvents)
})) }), true)
} }
} }
}; };

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Replays
protected override bool IsImportant(OsuReplayFrame frame) => frame.Actions.Any(); protected override bool IsImportant(OsuReplayFrame frame) => frame.Actions.Any();
public override void CollectPendingInputs(List<IInput> inputs) protected override void CollectReplayInputs(List<IInput> inputs)
{ {
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time); var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);

View File

@ -31,7 +31,8 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ProxyContainer approachCircles; private readonly ProxyContainer approachCircles;
private readonly ProxyContainer spinnerProxies; private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer; private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
private readonly FollowPointRenderer followPoints;
public FollowPointRenderer FollowPoints { get; }
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@ -50,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.UI
{ {
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both }, playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both }, spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }, FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both }, judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
HitObjectContainer, HitObjectContainer,
judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both }, judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both },
@ -131,13 +132,13 @@ namespace osu.Game.Rulesets.Osu.UI
protected override void OnHitObjectAdded(HitObject hitObject) protected override void OnHitObjectAdded(HitObject hitObject)
{ {
base.OnHitObjectAdded(hitObject); base.OnHitObjectAdded(hitObject);
followPoints.AddFollowPoints((OsuHitObject)hitObject); FollowPoints.AddFollowPoints((OsuHitObject)hitObject);
} }
protected override void OnHitObjectRemoved(HitObject hitObject) protected override void OnHitObjectRemoved(HitObject hitObject)
{ {
base.OnHitObjectRemoved(hitObject); base.OnHitObjectRemoved(hitObject);
followPoints.RemoveFollowPoints((OsuHitObject)hitObject); FollowPoints.RemoveFollowPoints((OsuHitObject)hitObject);
} }
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)

View File

@ -24,11 +24,16 @@
<string>armv7</string> <string>armv7</string>
</array> </array>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array> </array>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string> <string>Assets.xcassets/AppIcon.appiconset</string>

View File

@ -1,91 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
public class TestSceneEditorSaving : OsuGameTestScene
{
private Screens.Edit.Editor editor => Game.ChildrenOfType<Screens.Edit.Editor>().FirstOrDefault();
private EditorBeatmap editorBeatmap => (EditorBeatmap)editor.Dependencies.Get(typeof(EditorBeatmap));
/// <summary>
/// Tests the general expected flow of creating a new beatmap, saving it, then loading it back from song select.
/// Emphasis is placed on <see cref="BeatmapDifficulty.SliderMultiplier"/>, since taiko has special handling for it to keep compatibility with stable.
/// </summary>
[Test]
public void TestNewBeatmapSaveThenLoad()
{
AddStep("set default beatmap", () => Game.Beatmap.SetDefault());
AddStep("set taiko ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
PushAndConfirm(() => new EditorLoader());
AddUntilStep("wait for editor load", () => editor?.IsLoaded == true);
AddUntilStep("wait for metadata screen load", () => editor.ChildrenOfType<MetadataSection>().FirstOrDefault()?.IsLoaded == true);
// We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten.
AddStep("Enter compose mode", () => InputManager.Key(Key.F1));
AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true);
AddStep("Set slider multiplier", () => editorBeatmap.Difficulty.SliderMultiplier = 2);
AddStep("Set artist and title", () =>
{
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
editorBeatmap.BeatmapInfo.Metadata.Title = "title";
});
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty");
checkMutations();
AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
checkMutations();
AddStep("Exit", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
PushAndConfirm(() => new PlaySongSelect());
AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault);
AddStep("Open options", () => InputManager.Key(Key.F3));
AddStep("Enter editor", () => InputManager.Key(Key.Number5));
AddUntilStep("Wait for editor load", () => editor != null);
checkMutations();
}
private void checkMutations()
{
AddAssert("Beatmap has correct slider multiplier", () =>
{
// we can only assert value correctness on TaikoMultiplierAppliedDifficulty, because that is the final difficulty converted taiko beatmaps use.
// therefore, ensure that we have that difficulty type by calling .CopyFrom(), which is a no-op if the type is already correct.
var taikoDifficulty = new TaikoBeatmapConverter.TaikoMultiplierAppliedDifficulty();
taikoDifficulty.CopyFrom(editorBeatmap.Difficulty);
return Precision.AlmostEquals(taikoDifficulty.SliderMultiplier, 2);
});
AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
}
}
}

View File

@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
public class TestSceneTaikoEditorSaving : EditorSavingTestScene
{
protected override Ruleset CreateRuleset() => new TaikoRuleset();
[Test]
public void TestTaikoSliderMultiplier()
{
AddStep("Set slider multiplier", () => EditorBeatmap.Difficulty.SliderMultiplier = 2);
SaveEditor();
AddAssert("Beatmap has correct slider multiplier", assertTaikoSliderMulitplier);
ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct slider multiplier", assertTaikoSliderMulitplier);
bool assertTaikoSliderMulitplier()
{
// we can only assert value correctness on TaikoMultiplierAppliedDifficulty, because that is the final difficulty converted taiko beatmaps use.
// therefore, ensure that we have that difficulty type by calling .CopyFrom(), which is a no-op if the type is already correct.
var taikoDifficulty = new TaikoBeatmapConverter.TaikoMultiplierAppliedDifficulty();
taikoDifficulty.CopyFrom(EditorBeatmap.Difficulty);
return Precision.AlmostEquals(taikoDifficulty.SliderMultiplier, 2);
}
}
}
}

View File

@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken); Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken);
if (original.BeatmapInfo.RulesetID == 3) if (original.BeatmapInfo.Ruleset.OnlineID == 3)
{ {
// Post processing step to transform mania hit objects with the same start time into strong hits // Post processing step to transform mania hit objects with the same start time into strong hits
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x => converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>

View File

@ -4,6 +4,7 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.Taiko.UI;
@ -16,9 +17,26 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => 1.12;
private const float default_flashlight_size = 250; [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
{
MinValue = 0.5f,
MaxValue = 1.5f,
Default = 1f,
Value = 1f,
Precision = 0.1f
};
public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield); [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
public override BindableBool ComboBasedSize { get; } = new BindableBool
{
Default = true,
Value = true
};
public override float DefaultFlashlightSize => 250;
protected override Flashlight CreateFlashlight() => new TaikoFlashlight(this, playfield);
private TaikoPlayfield playfield; private TaikoPlayfield playfield;
@ -33,7 +51,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize); private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize);
private readonly TaikoPlayfield taikoPlayfield; private readonly TaikoPlayfield taikoPlayfield;
public TaikoFlashlight(TaikoPlayfield taikoPlayfield) public TaikoFlashlight(TaikoModFlashlight modFlashlight, TaikoPlayfield taikoPlayfield)
: base(modFlashlight)
{ {
this.taikoPlayfield = taikoPlayfield; this.taikoPlayfield = taikoPlayfield;
FlashlightSize = getSizeFor(0); FlashlightSize = getSizeFor(0);
@ -43,15 +62,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
private Vector2 getSizeFor(int combo) private Vector2 getSizeFor(int combo)
{ {
float size = default_flashlight_size;
if (combo > 200)
size *= 0.8f;
else if (combo > 100)
size *= 0.9f;
// Preserve flashlight size through the playfield's aspect adjustment. // Preserve flashlight size through the playfield's aspect adjustment.
return new Vector2(0, size * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT); return new Vector2(0, GetSizeFor(combo) * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
} }
protected override void OnComboChange(ValueChangedEvent<int> e) protected override void OnComboChange(ValueChangedEvent<int> e)

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Replays
protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any(); protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any();
public override void CollectPendingInputs(List<IInput> inputs) protected override void CollectReplayInputs(List<IInput> inputs)
{ {
inputs.Add(new ReplayState<TaikoAction> { PressedActions = CurrentFrame?.Actions ?? new List<TaikoAction>() }); inputs.Add(new ReplayState<TaikoAction> { PressedActions = CurrentFrame?.Actions ?? new List<TaikoAction>() });
} }

View File

@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
if (!effectPoint.KiaiMode) if (!effectPoint.KiaiMode)
return; return;
if (beatIndex % (int)timingPoint.TimeSignature != 0) if (beatIndex % timingPoint.TimeSignature.Numerator != 0)
return; return;
double duration = timingPoint.BeatLength * 2; double duration = timingPoint.BeatLength * 2;

View File

@ -213,21 +213,21 @@ namespace osu.Game.Rulesets.Taiko
{ {
Columns = new[] Columns = new[]
{ {
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents) new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 250 Height = 250
}), }, true),
} }
}, },
new StatisticRow new StatisticRow
{ {
Columns = new[] Columns = new[]
{ {
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{ {
new UnstableRate(timedHitEvents) new UnstableRate(timedHitEvents)
})) }), true)
} }
} }
}; };

View File

@ -24,11 +24,16 @@
<string>armv7</string> <string>armv7</string>
</array> </array>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array> </array>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string> <string>Assets.xcassets/AppIcon.appiconset</string>

View File

@ -60,7 +60,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(0, beatmapInfo.AudioLeadIn); Assert.AreEqual(0, beatmapInfo.AudioLeadIn);
Assert.AreEqual(164471, metadata.PreviewTime); Assert.AreEqual(164471, metadata.PreviewTime);
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); Assert.AreEqual(0.7f, beatmapInfo.StackLeniency);
Assert.IsTrue(beatmapInfo.RulesetID == 0); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0);
Assert.IsFalse(beatmapInfo.LetterboxInBreaks); Assert.IsFalse(beatmapInfo.LetterboxInBreaks);
Assert.IsFalse(beatmapInfo.SpecialStyle); Assert.IsFalse(beatmapInfo.SpecialStyle);
Assert.IsFalse(beatmapInfo.WidescreenStoryboard); Assert.IsFalse(beatmapInfo.WidescreenStoryboard);
@ -178,17 +178,17 @@ namespace osu.Game.Tests.Beatmaps.Formats
var timingPoint = controlPoints.TimingPointAt(0); var timingPoint = controlPoints.TimingPointAt(0);
Assert.AreEqual(956, timingPoint.Time); Assert.AreEqual(956, timingPoint.Time);
Assert.AreEqual(329.67032967033, timingPoint.BeatLength); Assert.AreEqual(329.67032967033, timingPoint.BeatLength);
Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature); Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
timingPoint = controlPoints.TimingPointAt(48428); timingPoint = controlPoints.TimingPointAt(48428);
Assert.AreEqual(956, timingPoint.Time); Assert.AreEqual(956, timingPoint.Time);
Assert.AreEqual(329.67032967033d, timingPoint.BeatLength); Assert.AreEqual(329.67032967033d, timingPoint.BeatLength);
Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature); Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
timingPoint = controlPoints.TimingPointAt(119637); timingPoint = controlPoints.TimingPointAt(119637);
Assert.AreEqual(119637, timingPoint.Time); Assert.AreEqual(119637, timingPoint.Time);
Assert.AreEqual(659.340659340659, timingPoint.BeatLength); Assert.AreEqual(659.340659340659, timingPoint.BeatLength);
Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature); Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
var difficultyPoint = controlPoints.DifficultyPointAt(0); var difficultyPoint = controlPoints.DifficultyPointAt(0);
Assert.AreEqual(0, difficultyPoint.Time); Assert.AreEqual(0, difficultyPoint.Time);
@ -794,5 +794,74 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(path.Distance, Is.EqualTo(1)); Assert.That(path.Distance, Is.EqualTo(1));
} }
} }
[Test]
public void TestLegacyDefaultsPreserved()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var memoryStream = new MemoryStream())
using (var stream = new LineBufferedReader(memoryStream))
{
var decoded = decoder.Decode(stream);
Assert.Multiple(() =>
{
Assert.That(decoded.BeatmapInfo.AudioLeadIn, Is.EqualTo(0));
Assert.That(decoded.BeatmapInfo.StackLeniency, Is.EqualTo(0.7f));
Assert.That(decoded.BeatmapInfo.SpecialStyle, Is.False);
Assert.That(decoded.BeatmapInfo.LetterboxInBreaks, Is.False);
Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False);
Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False);
Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False);
Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.Normal));
Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0));
Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1));
Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0));
});
}
}
[Test]
public void TestUndefinedApproachRateInheritsOverallDifficulty()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("undefined-approach-rate.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var decoded = decoder.Decode(stream);
Assert.That(decoded.Difficulty.ApproachRate, Is.EqualTo(1));
Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1));
}
}
[Test]
public void TestApproachRateDefinedBeforeOverallDifficulty()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("approach-rate-before-overall-difficulty.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var decoded = decoder.Decode(stream);
Assert.That(decoded.Difficulty.ApproachRate, Is.EqualTo(9));
Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1));
}
}
[Test]
public void TestApproachRateDefinedAfterOverallDifficulty()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("approach-rate-after-overall-difficulty.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var decoded = decoder.Decode(stream);
Assert.That(decoded.Difficulty.ApproachRate, Is.EqualTo(9));
Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1));
}
}
} }
} }

View File

@ -195,7 +195,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
private IBeatmap convert(IBeatmap beatmap) private IBeatmap convert(IBeatmap beatmap)
{ {
switch (beatmap.BeatmapInfo.RulesetID) switch (beatmap.BeatmapInfo.Ruleset.OnlineID)
{ {
case 0: case 0:
beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo; beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo;

View File

@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(0, beatmapInfo.AudioLeadIn); Assert.AreEqual(0, beatmapInfo.AudioLeadIn);
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); Assert.AreEqual(0.7f, beatmapInfo.StackLeniency);
Assert.AreEqual(false, beatmapInfo.SpecialStyle); Assert.AreEqual(false, beatmapInfo.SpecialStyle);
Assert.IsTrue(beatmapInfo.RulesetID == 0); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0);
Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks); Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks);
Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard); Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard);
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown);

View File

@ -53,9 +53,9 @@ namespace osu.Game.Tests.Beatmaps.IO
private static void ensureLoaded(OsuGameBase osu, int timeout = 60000) private static void ensureLoaded(OsuGameBase osu, int timeout = 60000)
{ {
var realmContextFactory = osu.Dependencies.Get<RealmContextFactory>(); var realm = osu.Dependencies.Get<RealmAccess>();
realmContextFactory.Run(realm => BeatmapImporterTests.EnsureLoaded(realm, timeout)); realm.Run(r => BeatmapImporterTests.EnsureLoaded(r, timeout));
// TODO: add back some extra checks outside of the realm ones? // TODO: add back some extra checks outside of the realm ones?
// var set = queryBeatmapSets().First(); // var set = queryBeatmapSets().First();

View File

@ -155,7 +155,7 @@ namespace osu.Game.Tests.Collections.IO
} }
// Name matches the automatically chosen name from `CleanRunHeadlessGameHost` above, so we end up using the same storage location. // Name matches the automatically chosen name from `CleanRunHeadlessGameHost` above, so we end up using the same storage location.
using (HeadlessGameHost host = new TestRunHeadlessGameHost(firstRunName)) using (HeadlessGameHost host = new TestRunHeadlessGameHost(firstRunName, null))
{ {
try try
{ {

View File

@ -38,12 +38,12 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestDetachBeatmapSet() public void TestDetachBeatmapSet()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using (var importer = new BeatmapModelManager(realmFactory, storage)) using (var importer = new BeatmapModelManager(realm, storage))
using (new RulesetStore(realmFactory, storage)) using (new RulesetStore(realm, storage))
{ {
ILive<BeatmapSetInfo>? beatmapSet; Live<BeatmapSetInfo>? beatmapSet;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
beatmapSet = await importer.Import(reader); beatmapSet = await importer.Import(reader);
@ -82,12 +82,12 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestUpdateDetachedBeatmapSet() public void TestUpdateDetachedBeatmapSet()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using (var importer = new BeatmapModelManager(realmFactory, storage)) using (var importer = new BeatmapModelManager(realm, storage))
using (new RulesetStore(realmFactory, storage)) using (new RulesetStore(realm, storage))
{ {
ILive<BeatmapSetInfo>? beatmapSet; Live<BeatmapSetInfo>? beatmapSet;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
beatmapSet = await importer.Import(reader); beatmapSet = await importer.Import(reader);
@ -139,53 +139,53 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestImportBeatmapThenCleanup() public void TestImportBeatmapThenCleanup()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using (var importer = new BeatmapModelManager(realmFactory, storage)) using (var importer = new BeatmapModelManager(realm, storage))
using (new RulesetStore(realmFactory, storage)) using (new RulesetStore(realm, storage))
{ {
ILive<BeatmapSetInfo>? imported; Live<BeatmapSetInfo>? imported;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
imported = await importer.Import(reader); imported = await importer.Import(reader);
Assert.AreEqual(1, realmFactory.Context.All<BeatmapSetInfo>().Count()); Assert.AreEqual(1, realm.Realm.All<BeatmapSetInfo>().Count());
Assert.NotNull(imported); Assert.NotNull(imported);
Debug.Assert(imported != null); Debug.Assert(imported != null);
imported.PerformWrite(s => s.DeletePending = true); imported.PerformWrite(s => s.DeletePending = true);
Assert.AreEqual(1, realmFactory.Context.All<BeatmapSetInfo>().Count(s => s.DeletePending)); Assert.AreEqual(1, realm.Realm.All<BeatmapSetInfo>().Count(s => s.DeletePending));
} }
}); });
Logger.Log("Running with no work to purge pending deletions"); Logger.Log("Running with no work to purge pending deletions");
RunTestWithRealm((realmFactory, _) => { Assert.AreEqual(0, realmFactory.Context.All<BeatmapSetInfo>().Count()); }); RunTestWithRealm((realm, _) => { Assert.AreEqual(0, realm.Realm.All<BeatmapSetInfo>().Count()); });
} }
[Test] [Test]
public void TestImportWhenClosed() public void TestImportWhenClosed()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
await LoadOszIntoStore(importer, realmFactory.Context); await LoadOszIntoStore(importer, realm.Realm);
}); });
} }
[Test] [Test]
public void TestAccessFileAfterImport() public void TestAccessFileAfterImport()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context); var imported = await LoadOszIntoStore(importer, realm.Realm);
var beatmap = imported.Beatmaps.First(); var beatmap = imported.Beatmaps.First();
var file = beatmap.File; var file = beatmap.File;
@ -198,33 +198,33 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestImportThenDelete() public void TestImportThenDelete()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context); var imported = await LoadOszIntoStore(importer, realm.Realm);
deleteBeatmapSet(imported, realmFactory.Context); deleteBeatmapSet(imported, realm.Realm);
}); });
} }
[Test] [Test]
public void TestImportThenDeleteFromStream() public void TestImportThenDeleteFromStream()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
string? tempPath = TestResources.GetTestBeatmapForImport(); string? tempPath = TestResources.GetTestBeatmapForImport();
ILive<BeatmapSetInfo>? importedSet; Live<BeatmapSetInfo>? importedSet;
using (var stream = File.OpenRead(tempPath)) using (var stream = File.OpenRead(tempPath))
{ {
importedSet = await importer.Import(new ImportTask(stream, Path.GetFileName(tempPath))); importedSet = await importer.Import(new ImportTask(stream, Path.GetFileName(tempPath)));
EnsureLoaded(realmFactory.Context); EnsureLoaded(realm.Realm);
} }
Assert.NotNull(importedSet); Assert.NotNull(importedSet);
@ -233,39 +233,39 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing"); Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing");
File.Delete(tempPath); File.Delete(tempPath);
var imported = realmFactory.Context.All<BeatmapSetInfo>().First(beatmapSet => beatmapSet.ID == importedSet.ID); var imported = realm.Realm.All<BeatmapSetInfo>().First(beatmapSet => beatmapSet.ID == importedSet.ID);
deleteBeatmapSet(imported, realmFactory.Context); deleteBeatmapSet(imported, realm.Realm);
}); });
} }
[Test] [Test]
public void TestImportThenImport() public void TestImportThenImport()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context); var imported = await LoadOszIntoStore(importer, realm.Realm);
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
Assert.IsTrue(imported.ID == importedSecondTime.ID); Assert.IsTrue(imported.ID == importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
checkBeatmapSetCount(realmFactory.Context, 1); checkBeatmapSetCount(realm.Realm, 1);
checkSingleReferencedFileCount(realmFactory.Context, 18); checkSingleReferencedFileCount(realm.Realm, 18);
}); });
} }
[Test] [Test]
public void TestImportThenImportWithReZip() public void TestImportThenImportWithReZip()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport(); string? temp = TestResources.GetTestBeatmapForImport();
@ -274,7 +274,7 @@ namespace osu.Game.Tests.Database
try try
{ {
var imported = await LoadOszIntoStore(importer, realmFactory.Context); var imported = await LoadOszIntoStore(importer, realm.Realm);
string hashBefore = hashFile(temp); string hashBefore = hashFile(temp);
@ -292,7 +292,7 @@ namespace osu.Game.Tests.Database
var importedSecondTime = await importer.Import(new ImportTask(temp)); var importedSecondTime = await importer.Import(new ImportTask(temp));
EnsureLoaded(realmFactory.Context); EnsureLoaded(realm.Realm);
Assert.NotNull(importedSecondTime); Assert.NotNull(importedSecondTime);
Debug.Assert(importedSecondTime != null); Debug.Assert(importedSecondTime != null);
@ -311,10 +311,10 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestImportThenImportWithChangedHashedFile() public void TestImportThenImportWithChangedHashedFile()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport(); string? temp = TestResources.GetTestBeatmapForImport();
@ -323,9 +323,9 @@ namespace osu.Game.Tests.Database
try try
{ {
var imported = await LoadOszIntoStore(importer, realmFactory.Context); var imported = await LoadOszIntoStore(importer, realm.Realm);
await createScoreForBeatmap(realmFactory.Context, imported.Beatmaps.First()); await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First());
using (var zip = ZipArchive.Open(temp)) using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder); zip.WriteToDirectory(extractedFolder);
@ -343,7 +343,7 @@ namespace osu.Game.Tests.Database
var importedSecondTime = await importer.Import(new ImportTask(temp)); var importedSecondTime = await importer.Import(new ImportTask(temp));
EnsureLoaded(realmFactory.Context); EnsureLoaded(realm.Realm);
// check the newly "imported" beatmap is not the original. // check the newly "imported" beatmap is not the original.
Assert.NotNull(importedSecondTime); Assert.NotNull(importedSecondTime);
@ -363,10 +363,10 @@ namespace osu.Game.Tests.Database
[Ignore("intentionally broken by import optimisations")] [Ignore("intentionally broken by import optimisations")]
public void TestImportThenImportWithChangedFile() public void TestImportThenImportWithChangedFile()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport(); string? temp = TestResources.GetTestBeatmapForImport();
@ -375,7 +375,7 @@ namespace osu.Game.Tests.Database
try try
{ {
var imported = await LoadOszIntoStore(importer, realmFactory.Context); var imported = await LoadOszIntoStore(importer, realm.Realm);
using (var zip = ZipArchive.Open(temp)) using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder); zip.WriteToDirectory(extractedFolder);
@ -392,7 +392,7 @@ namespace osu.Game.Tests.Database
var importedSecondTime = await importer.Import(new ImportTask(temp)); var importedSecondTime = await importer.Import(new ImportTask(temp));
EnsureLoaded(realmFactory.Context); EnsureLoaded(realm.Realm);
Assert.NotNull(importedSecondTime); Assert.NotNull(importedSecondTime);
Debug.Assert(importedSecondTime != null); Debug.Assert(importedSecondTime != null);
@ -411,10 +411,10 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestImportThenImportWithDifferentFilename() public void TestImportThenImportWithDifferentFilename()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport(); string? temp = TestResources.GetTestBeatmapForImport();
@ -423,7 +423,7 @@ namespace osu.Game.Tests.Database
try try
{ {
var imported = await LoadOszIntoStore(importer, realmFactory.Context); var imported = await LoadOszIntoStore(importer, realm.Realm);
using (var zip = ZipArchive.Open(temp)) using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder); zip.WriteToDirectory(extractedFolder);
@ -440,7 +440,7 @@ namespace osu.Game.Tests.Database
var importedSecondTime = await importer.Import(new ImportTask(temp)); var importedSecondTime = await importer.Import(new ImportTask(temp));
EnsureLoaded(realmFactory.Context); EnsureLoaded(realm.Realm);
Assert.NotNull(importedSecondTime); Assert.NotNull(importedSecondTime);
Debug.Assert(importedSecondTime != null); Debug.Assert(importedSecondTime != null);
@ -460,12 +460,12 @@ namespace osu.Game.Tests.Database
[Ignore("intentionally broken by import optimisations")] [Ignore("intentionally broken by import optimisations")]
public void TestImportCorruptThenImport() public void TestImportCorruptThenImport()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context); var imported = await LoadOszIntoStore(importer, realm.Realm);
var firstFile = imported.Files.First(); var firstFile = imported.Files.First();
@ -476,7 +476,7 @@ namespace osu.Game.Tests.Database
using (var stream = storage.GetStream(firstFile.File.GetStoragePath(), FileAccess.Write, FileMode.Create)) using (var stream = storage.GetStream(firstFile.File.GetStoragePath(), FileAccess.Write, FileMode.Create))
stream.WriteByte(0); stream.WriteByte(0);
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
using (var stream = storage.GetStream(firstFile.File.GetStoragePath())) using (var stream = storage.GetStream(firstFile.File.GetStoragePath()))
Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import"); Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import");
@ -485,18 +485,18 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.ID == importedSecondTime.ID); Assert.IsTrue(imported.ID == importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
checkBeatmapSetCount(realmFactory.Context, 1); checkBeatmapSetCount(realm.Realm, 1);
checkSingleReferencedFileCount(realmFactory.Context, 18); checkSingleReferencedFileCount(realm.Realm, 18);
}); });
} }
[Test] [Test]
public void TestModelCreationFailureDoesntReturn() public void TestModelCreationFailureDoesntReturn()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
var progressNotification = new ImportProgressNotification(); var progressNotification = new ImportProgressNotification();
@ -510,8 +510,8 @@ namespace osu.Game.Tests.Database
new ImportTask(zipStream, string.Empty) new ImportTask(zipStream, string.Empty)
); );
checkBeatmapSetCount(realmFactory.Context, 0); checkBeatmapSetCount(realm.Realm, 0);
checkBeatmapCount(realmFactory.Context, 0); checkBeatmapCount(realm.Realm, 0);
Assert.IsEmpty(imported); Assert.IsEmpty(imported);
Assert.AreEqual(ProgressNotificationState.Cancelled, progressNotification.State); Assert.AreEqual(ProgressNotificationState.Cancelled, progressNotification.State);
@ -521,7 +521,7 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestRollbackOnFailure() public void TestRollbackOnFailure()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
int loggedExceptionCount = 0; int loggedExceptionCount = 0;
@ -531,16 +531,16 @@ namespace osu.Game.Tests.Database
Interlocked.Increment(ref loggedExceptionCount); Interlocked.Increment(ref loggedExceptionCount);
}; };
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context); var imported = await LoadOszIntoStore(importer, realm.Realm);
realmFactory.Context.Write(() => imported.Hash += "-changed"); realm.Realm.Write(() => imported.Hash += "-changed");
checkBeatmapSetCount(realmFactory.Context, 1); checkBeatmapSetCount(realm.Realm, 1);
checkBeatmapCount(realmFactory.Context, 12); checkBeatmapCount(realm.Realm, 12);
checkSingleReferencedFileCount(realmFactory.Context, 18); checkSingleReferencedFileCount(realm.Realm, 18);
string? brokenTempFilename = TestResources.GetTestBeatmapForImport(); string? brokenTempFilename = TestResources.GetTestBeatmapForImport();
@ -565,10 +565,10 @@ namespace osu.Game.Tests.Database
{ {
} }
checkBeatmapSetCount(realmFactory.Context, 1); checkBeatmapSetCount(realm.Realm, 1);
checkBeatmapCount(realmFactory.Context, 12); checkBeatmapCount(realm.Realm, 12);
checkSingleReferencedFileCount(realmFactory.Context, 18); checkSingleReferencedFileCount(realm.Realm, 18);
Assert.AreEqual(1, loggedExceptionCount); Assert.AreEqual(1, loggedExceptionCount);
@ -579,18 +579,18 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestImportThenDeleteThenImportOptimisedPath() public void TestImportThenDeleteThenImportOptimisedPath()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context); var imported = await LoadOszIntoStore(importer, realm.Realm);
deleteBeatmapSet(imported, realmFactory.Context); deleteBeatmapSet(imported, realm.Realm);
Assert.IsTrue(imported.DeletePending); Assert.IsTrue(imported.DeletePending);
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
Assert.IsTrue(imported.ID == importedSecondTime.ID); Assert.IsTrue(imported.ID == importedSecondTime.ID);
@ -601,20 +601,52 @@ namespace osu.Game.Tests.Database
} }
[Test] [Test]
public void TestImportThenDeleteThenImportNonOptimisedPath() public void TestImportThenReimportAfterMissingFiles()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realmFactory, storage) =>
{ {
using var importer = new NonOptimisedBeatmapImporter(realmFactory, storage); using var importer = new BeatmapModelManager(realmFactory, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context); var imported = await LoadOszIntoStore(importer, realmFactory.Realm);
deleteBeatmapSet(imported, realmFactory.Context); deleteBeatmapSet(imported, realmFactory.Realm);
Assert.IsTrue(imported.DeletePending); Assert.IsTrue(imported.DeletePending);
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); // intentionally nuke all files
storage.DeleteDirectory("files");
Assert.That(imported.Files.All(f => !storage.GetStorageForDirectory("files").Exists(f.File.GetStoragePath())));
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Realm);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
Assert.IsTrue(imported.ID == importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
Assert.IsFalse(imported.DeletePending);
Assert.IsFalse(importedSecondTime.DeletePending);
// check that the files now exist, even though they were deleted above.
Assert.That(importedSecondTime.Files.All(f => storage.GetStorageForDirectory("files").Exists(f.File.GetStoragePath())));
});
}
[Test]
public void TestImportThenDeleteThenImportNonOptimisedPath()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new NonOptimisedBeatmapImporter(realm, storage);
using var store = new RulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
deleteBeatmapSet(imported, realm.Realm);
Assert.IsTrue(imported.DeletePending);
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
Assert.IsTrue(imported.ID == importedSecondTime.ID); Assert.IsTrue(imported.ID == importedSecondTime.ID);
@ -627,22 +659,22 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestImportThenDeleteThenImportWithOnlineIDsMissing() public void TestImportThenDeleteThenImportWithOnlineIDsMissing()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context); var imported = await LoadOszIntoStore(importer, realm.Realm);
realmFactory.Context.Write(() => realm.Realm.Write(() =>
{ {
foreach (var b in imported.Beatmaps) foreach (var b in imported.Beatmaps)
b.OnlineID = -1; b.OnlineID = -1;
}); });
deleteBeatmapSet(imported, realmFactory.Context); deleteBeatmapSet(imported, realm.Realm);
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
// check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched) // check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched)
Assert.IsTrue(imported.ID != importedSecondTime.ID); Assert.IsTrue(imported.ID != importedSecondTime.ID);
@ -653,10 +685,10 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestImportWithDuplicateBeatmapIDs() public void TestImportWithDuplicateBeatmapIDs()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealm((realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
var metadata = new BeatmapMetadata var metadata = new BeatmapMetadata
{ {
@ -667,7 +699,7 @@ namespace osu.Game.Tests.Database
} }
}; };
var ruleset = realmFactory.Context.All<RulesetInfo>().First(); var ruleset = realm.Realm.All<RulesetInfo>().First();
var toImport = new BeatmapSetInfo var toImport = new BeatmapSetInfo
{ {
@ -686,7 +718,7 @@ namespace osu.Game.Tests.Database
} }
}; };
var imported = await importer.Import(toImport); var imported = importer.Import(toImport);
Assert.NotNull(imported); Assert.NotNull(imported);
Debug.Assert(imported != null); Debug.Assert(imported != null);
@ -699,15 +731,15 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestImportWhenFileOpen() public void TestImportWhenFileOpen()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport(); string? temp = TestResources.GetTestBeatmapForImport();
using (File.OpenRead(temp)) using (File.OpenRead(temp))
await importer.Import(temp); await importer.Import(temp);
EnsureLoaded(realmFactory.Context); EnsureLoaded(realm.Realm);
File.Delete(temp); File.Delete(temp);
Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't");
}); });
@ -716,10 +748,10 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestImportWithDuplicateHashes() public void TestImportWithDuplicateHashes()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport(); string? temp = TestResources.GetTestBeatmapForImport();
@ -740,7 +772,7 @@ namespace osu.Game.Tests.Database
await importer.Import(temp); await importer.Import(temp);
EnsureLoaded(realmFactory.Context); EnsureLoaded(realm.Realm);
} }
finally finally
{ {
@ -752,10 +784,10 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestImportNestedStructure() public void TestImportNestedStructure()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport(); string? temp = TestResources.GetTestBeatmapForImport();
@ -780,7 +812,7 @@ namespace osu.Game.Tests.Database
Assert.NotNull(imported); Assert.NotNull(imported);
Debug.Assert(imported != null); Debug.Assert(imported != null);
EnsureLoaded(realmFactory.Context); EnsureLoaded(realm.Realm);
Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("subfolder"))), "Files contain common subfolder"); Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("subfolder"))), "Files contain common subfolder");
} }
@ -794,10 +826,10 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestImportWithIgnoredDirectoryInArchive() public void TestImportWithIgnoredDirectoryInArchive()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport(); string? temp = TestResources.GetTestBeatmapForImport();
@ -830,7 +862,7 @@ namespace osu.Game.Tests.Database
Assert.NotNull(imported); Assert.NotNull(imported);
Debug.Assert(imported != null); Debug.Assert(imported != null);
EnsureLoaded(realmFactory.Context); EnsureLoaded(realm.Realm);
Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("__MACOSX"))), "Files contain resource fork folder, which should be ignored"); Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("__MACOSX"))), "Files contain resource fork folder, which should be ignored");
Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("actual_data"))), "Files contain common subfolder"); Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("actual_data"))), "Files contain common subfolder");
@ -845,22 +877,22 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestUpdateBeatmapInfo() public void TestUpdateBeatmapInfo()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realm, storage) =>
{ {
using var importer = new BeatmapModelManager(realmFactory, storage); using var importer = new BeatmapModelManager(realm, storage);
using var store = new RulesetStore(realmFactory, storage); using var store = new RulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport(); string? temp = TestResources.GetTestBeatmapForImport();
await importer.Import(temp); await importer.Import(temp);
// Update via the beatmap, not the beatmap info, to ensure correct linking // Update via the beatmap, not the beatmap info, to ensure correct linking
BeatmapSetInfo setToUpdate = realmFactory.Context.All<BeatmapSetInfo>().First(); BeatmapSetInfo setToUpdate = realm.Realm.All<BeatmapSetInfo>().First();
var beatmapToUpdate = setToUpdate.Beatmaps.First(); var beatmapToUpdate = setToUpdate.Beatmaps.First();
realmFactory.Context.Write(() => beatmapToUpdate.DifficultyName = "updated"); realm.Realm.Write(() => beatmapToUpdate.DifficultyName = "updated");
BeatmapInfo updatedInfo = realmFactory.Context.All<BeatmapInfo>().First(b => b.ID == beatmapToUpdate.ID); BeatmapInfo updatedInfo = realm.Realm.All<BeatmapInfo>().First(b => b.ID == beatmapToUpdate.ID);
Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated")); Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated"));
}); });
} }
@ -1004,8 +1036,8 @@ namespace osu.Game.Tests.Database
public class NonOptimisedBeatmapImporter : BeatmapImporter public class NonOptimisedBeatmapImporter : BeatmapImporter
{ {
public NonOptimisedBeatmapImporter(RealmContextFactory realmFactory, Storage storage) public NonOptimisedBeatmapImporter(RealmAccess realm, Storage storage)
: base(realmFactory, storage) : base(realm, storage)
{ {
} }

View File

@ -19,10 +19,10 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestImportFile() public void TestImportFile()
{ {
RunTestWithRealm((realmFactory, storage) => RunTestWithRealm((realmAccess, storage) =>
{ {
var realm = realmFactory.Context; var realm = realmAccess.Realm;
var files = new RealmFileStore(realmFactory, storage); var files = new RealmFileStore(realmAccess, storage);
var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
@ -36,10 +36,10 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestImportSameFileTwice() public void TestImportSameFileTwice()
{ {
RunTestWithRealm((realmFactory, storage) => RunTestWithRealm((realmAccess, storage) =>
{ {
var realm = realmFactory.Context; var realm = realmAccess.Realm;
var files = new RealmFileStore(realmFactory, storage); var files = new RealmFileStore(realmAccess, storage);
var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
@ -53,10 +53,10 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestDontPurgeReferenced() public void TestDontPurgeReferenced()
{ {
RunTestWithRealm((realmFactory, storage) => RunTestWithRealm((realmAccess, storage) =>
{ {
var realm = realmFactory.Context; var realm = realmAccess.Realm;
var files = new RealmFileStore(realmFactory, storage); var files = new RealmFileStore(realmAccess, storage);
var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
@ -92,10 +92,10 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestPurgeUnreferenced() public void TestPurgeUnreferenced()
{ {
RunTestWithRealm((realmFactory, storage) => RunTestWithRealm((realmAccess, storage) =>
{ {
var realm = realmFactory.Context; var realm = realmAccess.Realm;
var files = new RealmFileStore(realmFactory, storage); var files = new RealmFileStore(realmAccess, storage);
var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));

View File

@ -21,15 +21,15 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestConstructRealm() public void TestConstructRealm()
{ {
RunTestWithRealm((realmFactory, _) => { realmFactory.Run(realm => realm.Refresh()); }); RunTestWithRealm((realm, _) => { realm.Run(r => r.Refresh()); });
} }
[Test] [Test]
public void TestBlockOperations() public void TestBlockOperations()
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realm, _) =>
{ {
using (realmFactory.BlockAllOperations()) using (realm.BlockAllOperations())
{ {
} }
}); });
@ -42,24 +42,25 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestNestedContextCreationWithSubscription() public void TestNestedContextCreationWithSubscription()
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realm, _) =>
{ {
bool callbackRan = false; bool callbackRan = false;
realmFactory.Run(realm => realm.RegisterCustomSubscription(r =>
{ {
var subscription = realm.All<BeatmapInfo>().QueryAsyncWithNotifications((sender, changes, error) => var subscription = r.All<BeatmapInfo>().QueryAsyncWithNotifications((sender, changes, error) =>
{ {
realmFactory.Run(_ => realm.Run(_ =>
{ {
callbackRan = true; callbackRan = true;
}); });
}); });
// Force the callback above to run. // Force the callback above to run.
realmFactory.Run(r => r.Refresh()); realm.Run(rr => rr.Refresh());
subscription?.Dispose(); subscription?.Dispose();
return null;
}); });
Assert.IsTrue(callbackRan); Assert.IsTrue(callbackRan);
@ -69,14 +70,14 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestBlockOperationsWithContention() public void TestBlockOperationsWithContention()
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realm, _) =>
{ {
ManualResetEventSlim stopThreadedUsage = new ManualResetEventSlim(); ManualResetEventSlim stopThreadedUsage = new ManualResetEventSlim();
ManualResetEventSlim hasThreadedUsage = new ManualResetEventSlim(); ManualResetEventSlim hasThreadedUsage = new ManualResetEventSlim();
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
realmFactory.Run(_ => realm.Run(_ =>
{ {
hasThreadedUsage.Set(); hasThreadedUsage.Set();
@ -88,12 +89,17 @@ namespace osu.Game.Tests.Database
Assert.Throws<TimeoutException>(() => Assert.Throws<TimeoutException>(() =>
{ {
using (realmFactory.BlockAllOperations()) using (realm.BlockAllOperations())
{ {
} }
}); });
stopThreadedUsage.Set(); stopThreadedUsage.Set();
// Ensure we can block a second time after the usage has ended.
using (realm.BlockAllOperations())
{
}
}); });
} }
} }

View File

@ -21,11 +21,11 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestLiveEquality() public void TestLiveEquality()
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realm, _) =>
{ {
ILive<BeatmapInfo> beatmap = realmFactory.Run(realm => realm.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realmFactory)); Live<BeatmapInfo> beatmap = realm.Run(r => r.Write(_ => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realm));
ILive<BeatmapInfo> beatmap2 = realmFactory.Run(realm => realm.All<BeatmapInfo>().First().ToLive(realmFactory)); Live<BeatmapInfo> beatmap2 = realm.Run(r => r.All<BeatmapInfo>().First().ToLive(realm));
Assert.AreEqual(beatmap, beatmap2); Assert.AreEqual(beatmap, beatmap2);
}); });
@ -34,29 +34,27 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestAccessAfterStorageMigrate() public void TestAccessAfterStorageMigrate()
{ {
RunTestWithRealm((realmFactory, storage) => RunTestWithRealm((realm, storage) =>
{ {
var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
ILive<BeatmapInfo>? liveBeatmap = null; Live<BeatmapInfo>? liveBeatmap = null;
realmFactory.Run(realm => realm.Run(r =>
{ {
realm.Write(r => r.Add(beatmap)); r.Write(_ => r.Add(beatmap));
liveBeatmap = beatmap.ToLive(realmFactory); liveBeatmap = beatmap.ToLive(realm);
}); });
using (realmFactory.BlockAllOperations())
{
// recycle realm before migrating
}
using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target")) using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target"))
{ {
migratedStorage.DeleteDirectory(string.Empty); migratedStorage.DeleteDirectory(string.Empty);
storage.Migrate(migratedStorage); using (realm.BlockAllOperations())
{
storage.Migrate(migratedStorage);
}
Assert.IsFalse(liveBeatmap?.PerformRead(l => l.Hidden)); Assert.IsFalse(liveBeatmap?.PerformRead(l => l.Hidden));
} }
@ -66,13 +64,13 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestAccessAfterAttach() public void TestAccessAfterAttach()
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realm, _) =>
{ {
var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
var liveBeatmap = beatmap.ToLive(realmFactory); var liveBeatmap = beatmap.ToLive(realm);
realmFactory.Run(realm => realm.Write(r => r.Add(beatmap))); realm.Run(r => r.Write(_ => r.Add(beatmap)));
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
}); });
@ -98,16 +96,16 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestScopedReadWithoutContext() public void TestScopedReadWithoutContext()
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realm, _) =>
{ {
ILive<BeatmapInfo>? liveBeatmap = null; Live<BeatmapInfo>? liveBeatmap = null;
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
realmFactory.Run(threadContext => realm.Run(threadContext =>
{ {
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
liveBeatmap = beatmap.ToLive(realmFactory); liveBeatmap = beatmap.ToLive(realm);
}); });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
@ -127,16 +125,16 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestScopedWriteWithoutContext() public void TestScopedWriteWithoutContext()
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realm, _) =>
{ {
ILive<BeatmapInfo>? liveBeatmap = null; Live<BeatmapInfo>? liveBeatmap = null;
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
realmFactory.Run(threadContext => realm.Run(threadContext =>
{ {
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
liveBeatmap = beatmap.ToLive(realmFactory); liveBeatmap = beatmap.ToLive(realm);
}); });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
@ -153,10 +151,10 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestValueAccessNonManaged() public void TestValueAccessNonManaged()
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realm, _) =>
{ {
var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
var liveBeatmap = beatmap.ToLive(realmFactory); var liveBeatmap = beatmap.ToLive(realm);
Assert.DoesNotThrow(() => Assert.DoesNotThrow(() =>
{ {
@ -168,17 +166,17 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestValueAccessWithOpenContextFails() public void TestValueAccessWithOpenContextFails()
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realm, _) =>
{ {
ILive<BeatmapInfo>? liveBeatmap = null; Live<BeatmapInfo>? liveBeatmap = null;
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
realmFactory.Run(threadContext => realm.Run(threadContext =>
{ {
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
liveBeatmap = beatmap.ToLive(realmFactory); liveBeatmap = beatmap.ToLive(realm);
}); });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
@ -193,7 +191,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.
realmFactory.Run(threadContext => realm.Run(threadContext =>
{ {
Assert.Throws<InvalidOperationException>(() => Assert.Throws<InvalidOperationException>(() =>
{ {
@ -207,16 +205,16 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestValueAccessWithoutOpenContextFails() public void TestValueAccessWithoutOpenContextFails()
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realm, _) =>
{ {
ILive<BeatmapInfo>? liveBeatmap = null; Live<BeatmapInfo>? liveBeatmap = null;
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
realmFactory.Run(threadContext => realm.Run(threadContext =>
{ {
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
liveBeatmap = beatmap.ToLive(realmFactory); liveBeatmap = beatmap.ToLive(realm);
}); });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
@ -235,18 +233,18 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestLiveAssumptions() public void TestLiveAssumptions()
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realm, _) =>
{ {
int changesTriggered = 0; int changesTriggered = 0;
realmFactory.Run(outerRealm => realm.RegisterCustomSubscription(outerRealm =>
{ {
outerRealm.All<BeatmapInfo>().QueryAsyncWithNotifications(gotChange); outerRealm.All<BeatmapInfo>().QueryAsyncWithNotifications(gotChange);
ILive<BeatmapInfo>? liveBeatmap = null; Live<BeatmapInfo>? liveBeatmap = null;
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
realmFactory.Run(innerRealm => realm.Run(innerRealm =>
{ {
var ruleset = CreateRuleset(); var ruleset = CreateRuleset();
var beatmap = innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata()))); var beatmap = innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata())));
@ -255,7 +253,7 @@ namespace osu.Game.Tests.Database
// not just a refresh from the resolved Live. // not just a refresh from the resolved Live.
innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata()))); innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata())));
liveBeatmap = beatmap.ToLive(realmFactory); liveBeatmap = beatmap.ToLive(realm);
}); });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
@ -282,6 +280,8 @@ namespace osu.Game.Tests.Database
r.Remove(resolved); r.Remove(resolved);
}); });
}); });
return null;
}); });
void gotChange(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error) void gotChange(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error)

View File

@ -0,0 +1,138 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
using Realms;
#nullable enable
namespace osu.Game.Tests.Database
{
[TestFixture]
public class RealmSubscriptionRegistrationTests : RealmTest
{
[Test]
public void TestSubscriptionWithContextLoss()
{
IEnumerable<BeatmapSetInfo>? resolvedItems = null;
ChangeSet? lastChanges = null;
RunTestWithRealm((realm, _) =>
{
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
var registration = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>(), onChanged);
testEventsArriving(true);
// All normal until here.
// Now let's yank the main realm context.
resolvedItems = null;
lastChanges = null;
using (realm.BlockAllOperations())
Assert.That(resolvedItems, Is.Empty);
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
testEventsArriving(true);
// Now let's try unsubscribing.
resolvedItems = null;
lastChanges = null;
registration.Dispose();
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
testEventsArriving(false);
// And make sure even after another context loss we don't get firings.
using (realm.BlockAllOperations())
Assert.That(resolvedItems, Is.Null);
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
testEventsArriving(false);
void testEventsArriving(bool shouldArrive)
{
realm.Run(r => r.Refresh());
if (shouldArrive)
Assert.That(resolvedItems, Has.One.Items);
else
Assert.That(resolvedItems, Is.Null);
realm.Write(r =>
{
r.RemoveAll<BeatmapSetInfo>();
r.RemoveAll<RulesetInfo>();
});
realm.Run(r => r.Refresh());
if (shouldArrive)
Assert.That(lastChanges?.DeletedIndices, Has.One.Items);
else
Assert.That(lastChanges, Is.Null);
}
});
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error)
{
if (changes == null)
resolvedItems = sender;
lastChanges = changes;
}
}
[Test]
public void TestCustomRegisterWithContextLoss()
{
RunTestWithRealm((realm, _) =>
{
BeatmapSetInfo? beatmapSetInfo = null;
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
var subscription = realm.RegisterCustomSubscription(r =>
{
beatmapSetInfo = r.All<BeatmapSetInfo>().First();
return new InvokeOnDisposal(() => beatmapSetInfo = null);
});
Assert.That(beatmapSetInfo, Is.Not.Null);
using (realm.BlockAllOperations())
{
// custom disposal action fired when context lost.
Assert.That(beatmapSetInfo, Is.Null);
}
// re-registration after context restore.
realm.Run(r => r.Refresh());
Assert.That(beatmapSetInfo, Is.Not.Null);
subscription.Dispose();
Assert.That(beatmapSetInfo, Is.Null);
using (realm.BlockAllOperations())
Assert.That(beatmapSetInfo, Is.Null);
realm.Run(r => r.Refresh());
Assert.That(beatmapSetInfo, Is.Null);
});
}
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Tests.Database
storage.DeleteDirectory(string.Empty); storage.DeleteDirectory(string.Empty);
} }
protected void RunTestWithRealm(Action<RealmContextFactory, OsuStorage> testAction, [CallerMemberName] string caller = "") protected void RunTestWithRealm(Action<RealmAccess, OsuStorage> testAction, [CallerMemberName] string caller = "")
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(callingMethodName: caller)) using (HeadlessGameHost host = new CleanRunHeadlessGameHost(callingMethodName: caller))
{ {
@ -39,22 +39,22 @@ namespace osu.Game.Tests.Database
// ReSharper disable once AccessToDisposedClosure // ReSharper disable once AccessToDisposedClosure
var testStorage = new OsuStorage(host, storage.GetStorageForDirectory(caller)); var testStorage = new OsuStorage(host, storage.GetStorageForDirectory(caller));
using (var realmFactory = new RealmContextFactory(testStorage, "client")) using (var realm = new RealmAccess(testStorage, "client"))
{ {
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}");
testAction(realmFactory, testStorage); testAction(realm, testStorage);
realmFactory.Dispose(); realm.Dispose();
Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); Logger.Log($"Final database size: {getFileSize(testStorage, realm)}");
realmFactory.Compact(); realm.Compact();
Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); Logger.Log($"Final database size after compact: {getFileSize(testStorage, realm)}");
} }
})); }));
} }
} }
protected void RunTestWithRealmAsync(Func<RealmContextFactory, Storage, Task> testAction, [CallerMemberName] string caller = "") protected void RunTestWithRealmAsync(Func<RealmAccess, Storage, Task> testAction, [CallerMemberName] string caller = "")
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(callingMethodName: caller)) using (HeadlessGameHost host = new CleanRunHeadlessGameHost(callingMethodName: caller))
{ {
@ -62,15 +62,15 @@ namespace osu.Game.Tests.Database
{ {
var testStorage = storage.GetStorageForDirectory(caller); var testStorage = storage.GetStorageForDirectory(caller);
using (var realmFactory = new RealmContextFactory(testStorage, "client")) using (var realm = new RealmAccess(testStorage, "client"))
{ {
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}");
await testAction(realmFactory, testStorage); await testAction(realm, testStorage);
realmFactory.Dispose(); realm.Dispose();
Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); Logger.Log($"Final database size: {getFileSize(testStorage, realm)}");
realmFactory.Compact(); realm.Compact();
} }
})); }));
} }
@ -114,7 +114,7 @@ namespace osu.Game.Tests.Database
} }
protected static RulesetInfo CreateRuleset() => protected static RulesetInfo CreateRuleset() =>
new RulesetInfo(0, "osu!", "osu", true); new RulesetInfo("osu", "osu!", string.Empty, 0) { Available = true };
private class RealmTestGame : Framework.Game private class RealmTestGame : Framework.Game
{ {
@ -138,11 +138,11 @@ namespace osu.Game.Tests.Database
} }
} }
private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory) private static long getFileSize(Storage testStorage, RealmAccess realm)
{ {
try try
{ {
using (var stream = testStorage.GetStream(realmFactory.Filename)) using (var stream = testStorage.GetStream(realm.Filename))
return stream?.Length ?? 0; return stream?.Length ?? 0;
} }
catch catch

View File

@ -12,37 +12,37 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestCreateStore() public void TestCreateStore()
{ {
RunTestWithRealm((realmFactory, storage) => RunTestWithRealm((realm, storage) =>
{ {
var rulesets = new RulesetStore(realmFactory, storage); var rulesets = new RulesetStore(realm, storage);
Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
Assert.AreEqual(4, realmFactory.Context.All<RulesetInfo>().Count()); Assert.AreEqual(4, realm.Realm.All<RulesetInfo>().Count());
}); });
} }
[Test] [Test]
public void TestCreateStoreTwiceDoesntAddRulesetsAgain() public void TestCreateStoreTwiceDoesntAddRulesetsAgain()
{ {
RunTestWithRealm((realmFactory, storage) => RunTestWithRealm((realm, storage) =>
{ {
var rulesets = new RulesetStore(realmFactory, storage); var rulesets = new RulesetStore(realm, storage);
var rulesets2 = new RulesetStore(realmFactory, storage); var rulesets2 = new RulesetStore(realm, storage);
Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
Assert.AreEqual(4, rulesets2.AvailableRulesets.Count()); Assert.AreEqual(4, rulesets2.AvailableRulesets.Count());
Assert.AreEqual(rulesets.AvailableRulesets.First(), rulesets2.AvailableRulesets.First()); Assert.AreEqual(rulesets.AvailableRulesets.First(), rulesets2.AvailableRulesets.First());
Assert.AreEqual(4, realmFactory.Context.All<RulesetInfo>().Count()); Assert.AreEqual(4, realm.Realm.All<RulesetInfo>().Count());
}); });
} }
[Test] [Test]
public void TestRetrievedRulesetsAreDetached() public void TestRetrievedRulesetsAreDetached()
{ {
RunTestWithRealm((realmFactory, storage) => RunTestWithRealm((realm, storage) =>
{ {
var rulesets = new RulesetStore(realmFactory, storage); var rulesets = new RulesetStore(realm, storage);
Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged); Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged);
Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged); Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged);

View File

@ -24,7 +24,7 @@ namespace osu.Game.Tests.Database
private RealmKeyBindingStore keyBindingStore; private RealmKeyBindingStore keyBindingStore;
private RealmContextFactory realmContextFactory; private RealmAccess realm;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
@ -33,8 +33,8 @@ namespace osu.Game.Tests.Database
storage = new NativeStorage(directory.FullName); storage = new NativeStorage(directory.FullName);
realmContextFactory = new RealmContextFactory(storage, "test"); realm = new RealmAccess(storage, "test");
keyBindingStore = new RealmKeyBindingStore(realmContextFactory, new ReadableKeyCombinationProvider()); keyBindingStore = new RealmKeyBindingStore(realm, new ReadableKeyCombinationProvider());
} }
[Test] [Test]
@ -60,11 +60,11 @@ namespace osu.Game.Tests.Database
KeyBindingContainer testContainer = new TestKeyBindingContainer(); KeyBindingContainer testContainer = new TestKeyBindingContainer();
// Add some excess bindings for an action which only supports 1. // Add some excess bindings for an action which only supports 1.
realmContextFactory.Write(realm => realm.Write(r =>
{ {
realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.A))); r.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.A)));
realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.S))); r.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.S)));
realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.D))); r.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.D)));
}); });
Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(3)); Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(3));
@ -76,9 +76,9 @@ namespace osu.Game.Tests.Database
private int queryCount(GlobalAction? match = null) private int queryCount(GlobalAction? match = null)
{ {
return realmContextFactory.Run(realm => return realm.Run(r =>
{ {
var results = realm.All<RealmKeyBinding>(); var results = r.All<RealmKeyBinding>();
if (match.HasValue) if (match.HasValue)
results = results.Where(k => k.ActionInt == (int)match.Value); results = results.Where(k => k.ActionInt == (int)match.Value);
return results.Count(); return results.Count();
@ -92,7 +92,7 @@ namespace osu.Game.Tests.Database
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>()); keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
realmContextFactory.Run(outerRealm => realm.Run(outerRealm =>
{ {
var backBinding = outerRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back); var backBinding = outerRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
@ -100,7 +100,7 @@ namespace osu.Game.Tests.Database
var tsr = ThreadSafeReference.Create(backBinding); var tsr = ThreadSafeReference.Create(backBinding);
realmContextFactory.Run(innerRealm => realm.Run(innerRealm =>
{ {
var binding = innerRealm.ResolveReference(tsr); var binding = innerRealm.ResolveReference(tsr);
innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
@ -117,7 +117,7 @@ namespace osu.Game.Tests.Database
[TearDown] [TearDown]
public void TearDown() public void TearDown()
{ {
realmContextFactory.Dispose(); realm.Dispose();
storage.DeleteDirectory(string.Empty); storage.DeleteDirectory(string.Empty);
} }

View File

@ -52,7 +52,7 @@ namespace osu.Game.Tests.Editing.Checks
beatmap.BeatmapInfo.BeatmapSet.Files.Add(CheckTestHelpers.CreateMockFile("jpg")); beatmap.BeatmapInfo.BeatmapSet.Files.Add(CheckTestHelpers.CreateMockFile("jpg"));
// Should fail to load, but not produce an error due to the extension not being expected to load. // Should fail to load, but not produce an error due to the extension not being expected to load.
Assert.IsEmpty(check.Run(getContext(null, allowMissing: true))); Assert.IsEmpty(check.Run(getContext(null)));
} }
[Test] [Test]
@ -91,7 +91,7 @@ namespace osu.Game.Tests.Editing.Checks
{ {
using (var resourceStream = TestResources.OpenResource("Samples/missing.mp3")) using (var resourceStream = TestResources.OpenResource("Samples/missing.mp3"))
{ {
Assert.IsEmpty(check.Run(getContext(resourceStream, allowMissing: true))); Assert.IsEmpty(check.Run(getContext(resourceStream)));
} }
} }
@ -107,7 +107,7 @@ namespace osu.Game.Tests.Editing.Checks
} }
} }
private BeatmapVerifierContext getContext(Stream resourceStream, bool allowMissing = false) private BeatmapVerifierContext getContext(Stream resourceStream)
{ {
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null); var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream); mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);

View File

@ -131,8 +131,6 @@ namespace osu.Game.Tests.Gameplay
public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{ {
switch (lookup) switch (lookup)

View File

@ -2,7 +2,13 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -12,47 +18,93 @@ namespace osu.Game.Tests.Gameplay
[HeadlessTest] [HeadlessTest]
public class TestSceneMasterGameplayClockContainer : OsuTestScene public class TestSceneMasterGameplayClockContainer : OsuTestScene
{ {
private OsuConfigManager localConfig;
[BackgroundDependencyLoader]
private void load()
{
Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
}
[Test] [Test]
public void TestStartThenElapsedTime() public void TestStartThenElapsedTime()
{ {
GameplayClockContainer gcc = null; GameplayClockContainer gameplayClockContainer = null;
AddStep("create container", () => AddStep("create container", () =>
{ {
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack(); working.LoadTrack();
Add(gcc = new MasterGameplayClockContainer(working, 0)); Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
}); });
AddStep("start clock", () => gcc.Start()); AddStep("start clock", () => gameplayClockContainer.Start());
AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0); AddUntilStep("elapsed greater than zero", () => gameplayClockContainer.GameplayClock.ElapsedFrameTime > 0);
} }
[Test] [Test]
public void TestElapseThenReset() public void TestElapseThenReset()
{ {
GameplayClockContainer gcc = null; GameplayClockContainer gameplayClockContainer = null;
AddStep("create container", () => AddStep("create container", () =>
{ {
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack(); working.LoadTrack();
Add(gcc = new MasterGameplayClockContainer(working, 0)); Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
}); });
AddStep("start clock", () => gcc.Start()); AddStep("start clock", () => gameplayClockContainer.Start());
AddUntilStep("current time greater 2000", () => gcc.GameplayClock.CurrentTime > 2000); AddUntilStep("current time greater 2000", () => gameplayClockContainer.GameplayClock.CurrentTime > 2000);
double timeAtReset = 0; double timeAtReset = 0;
AddStep("reset clock", () => AddStep("reset clock", () =>
{ {
timeAtReset = gcc.GameplayClock.CurrentTime; timeAtReset = gameplayClockContainer.GameplayClock.CurrentTime;
gcc.Reset(); gameplayClockContainer.Reset();
}); });
AddAssert("current time < time at reset", () => gcc.GameplayClock.CurrentTime < timeAtReset); AddAssert("current time < time at reset", () => gameplayClockContainer.GameplayClock.CurrentTime < timeAtReset);
}
[Test]
public void TestSeekPerformsInGameplayTime(
[Values(1.0, 0.5, 2.0)] double clockRate,
[Values(0.0, 200.0, -200.0)] double userOffset,
[Values(false, true)] bool whileStopped)
{
ClockBackedTestWorkingBeatmap working = null;
GameplayClockContainer gameplayClockContainer = null;
AddStep("create container", () =>
{
working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio);
working.LoadTrack();
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
if (whileStopped)
gameplayClockContainer.Stop();
gameplayClockContainer.Reset();
});
AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(clockRate)));
AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500));
AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f));
AddStep("seek to 10000", () => gameplayClockContainer.Seek(10000));
AddAssert("gameplay clock time = 10000", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 10000, 10f));
}
protected override void Dispose(bool isDisposing)
{
localConfig?.Dispose();
base.Dispose(isDisposing);
} }
} }
} }

View File

@ -1,11 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -42,6 +48,43 @@ namespace osu.Game.Tests.Gameplay
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(Judgement.LARGE_BONUS_SCORE)); Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(Judgement.LARGE_BONUS_SCORE));
} }
[Test]
public void TestResetFromReplayFrame()
{
var beatmap = new Beatmap<HitObject> { HitObjects = { new HitCircle() } };
var scoreProcessor = new ScoreProcessor();
scoreProcessor.ApplyBeatmap(beatmap);
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TestJudgement(HitResult.Great)) { Type = HitResult.Great });
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
// No header shouldn't cause any change
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame());
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
// Reset with a miss instead.
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame
{
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int> { { HitResult.Miss, 1 } }, DateTimeOffset.Now)
});
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
// Reset with no judged hit.
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame
{
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int>(), DateTimeOffset.Now)
});
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
Assert.That(scoreProcessor.JudgedHits, Is.Zero);
}
private class TestJudgement : Judgement private class TestJudgement : Judgement
{ {
public override HitResult MaxResult { get; } public override HitResult MaxResult { get; }

View File

@ -261,7 +261,7 @@ namespace osu.Game.Tests.Gameplay
public AudioManager AudioManager => Audio; public AudioManager AudioManager => Audio;
public IResourceStore<byte[]> Files => null; public IResourceStore<byte[]> Files => null;
public new IResourceStore<byte[]> Resources => base.Resources; public new IResourceStore<byte[]> Resources => base.Resources;
public RealmContextFactory RealmContextFactory => null; public RealmAccess RealmAccess => null;
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => null; public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => null;
#endregion #endregion

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tests.NonVisual
const int beat_length_numerator = 2000; const int beat_length_numerator = 2000;
const int beat_length_denominator = 7; const int beat_length_denominator = 7;
const TimeSignatures signature = TimeSignatures.SimpleQuadruple; TimeSignature signature = TimeSignature.SimpleQuadruple;
var beatmap = new Beatmap var beatmap = new Beatmap
{ {
@ -49,7 +49,7 @@ namespace osu.Game.Tests.NonVisual
for (int i = 0; i * beat_length_denominator < barLines.Count; i++) for (int i = 0; i * beat_length_denominator < barLines.Count; i++)
{ {
var barLine = barLines[i * beat_length_denominator]; var barLine = barLines[i * beat_length_denominator];
int expectedTime = beat_length_numerator * (int)signature * i; int expectedTime = beat_length_numerator * signature.Numerator * i;
// every seventh bar's start time should be at least greater than the whole number we expect. // every seventh bar's start time should be at least greater than the whole number we expect.
// It cannot be less, as that can affect overlapping scroll algorithms // It cannot be less, as that can affect overlapping scroll algorithms
@ -60,7 +60,7 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(Precision.AlmostEquals(barLine.StartTime, expectedTime)); Assert.IsTrue(Precision.AlmostEquals(barLine.StartTime, expectedTime));
// check major/minor lines for good measure too // check major/minor lines for good measure too
Assert.AreEqual(i % (int)signature == 0, barLine.Major); Assert.AreEqual(i % signature.Numerator == 0, barLine.Major);
} }
} }

View File

@ -142,19 +142,28 @@ namespace osu.Game.Tests.NonVisual
Assert.That(osuStorage, Is.Not.Null); Assert.That(osuStorage, Is.Not.Null);
// In the following tests, realm files are ignored as
// - in the case of checking the source, interacting with the pipe files (client.realm.note) may
// lead to unexpected behaviour.
// - in the case of checking the destination, the files may have already been recreated by the game
// as part of the standard migration flow.
foreach (string file in osuStorage.IgnoreFiles) foreach (string file in osuStorage.IgnoreFiles)
{ {
// avoid touching realm files which may be a pipe and break everything. if (!file.Contains("realm", StringComparison.Ordinal))
// this is also done locally inside OsuStorage via the IgnoreFiles list. {
if (file.EndsWith(".ini", StringComparison.Ordinal))
Assert.That(File.Exists(Path.Combine(originalDirectory, file))); Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
Assert.That(storage.Exists(file), Is.False); Assert.That(storage.Exists(file), Is.False, () => $"{file} exists in destination when it was expected to be ignored");
}
} }
foreach (string dir in osuStorage.IgnoreDirectories) foreach (string dir in osuStorage.IgnoreDirectories)
{ {
Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir))); if (!dir.Contains("realm", StringComparison.Ordinal))
Assert.That(storage.ExistsDirectory(dir), Is.False); {
Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
Assert.That(storage.Exists(dir), Is.False, () => $"{dir} exists in destination when it was expected to be ignored");
}
} }
Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}")); Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}"));

View File

@ -16,7 +16,11 @@ namespace osu.Game.Tests.NonVisual.Filtering
{ {
private BeatmapInfo getExampleBeatmap() => new BeatmapInfo private BeatmapInfo getExampleBeatmap() => new BeatmapInfo
{ {
Ruleset = new RulesetInfo { OnlineID = 0 }, Ruleset = new RulesetInfo
{
ShortName = "osu",
OnlineID = 0
},
StarRating = 4.0d, StarRating = 4.0d,
Difficulty = new BeatmapDifficulty Difficulty = new BeatmapDifficulty
{ {
@ -57,7 +61,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
var exampleBeatmapInfo = getExampleBeatmap(); var exampleBeatmapInfo = getExampleBeatmap();
var criteria = new FilterCriteria var criteria = new FilterCriteria
{ {
Ruleset = new RulesetInfo { OnlineID = 6 } Ruleset = new RulesetInfo { ShortName = "catch" }
}; };
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
carouselItem.Filter(criteria); carouselItem.Filter(criteria);
@ -78,6 +82,20 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.IsFalse(carouselItem.Filtered.Value); Assert.IsFalse(carouselItem.Filtered.Value);
} }
[Test]
public void TestCriteriaMatchingConvertedBeatmapsForCustomRulesets()
{
var exampleBeatmapInfo = getExampleBeatmap();
var criteria = new FilterCriteria
{
Ruleset = new RulesetInfo { OnlineID = -1 },
AllowConvertedBeatmaps = true
};
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
carouselItem.Filter(criteria);
Assert.IsFalse(carouselItem.Filtered.Value);
}
[Test] [Test]
[TestCase(true)] [TestCase(true)]
[TestCase(false)] [TestCase(false)]

View File

@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class RulesetInfoOrderingTest
{
[Test]
public void TestOrdering()
{
var rulesets = new[]
{
new RulesetInfo("custom2", "Custom Ruleset 2", string.Empty, -1),
new OsuRuleset().RulesetInfo,
new RulesetInfo("custom3", "Custom Ruleset 3", string.Empty, -1),
new RulesetInfo("custom2", "Custom Ruleset 2", string.Empty, -1),
new CatchRuleset().RulesetInfo,
new RulesetInfo("custom3", "Custom Ruleset 3", string.Empty, -1),
};
var orderedRulesets = rulesets.OrderBy(r => r);
// Ensure all customs are after official.
Assert.That(orderedRulesets.Select(r => r.OnlineID), Is.EqualTo(new[] { 0, 2, -1, -1, -1, -1 }));
// Ensure customs are grouped next to each other (ie. stably sorted).
Assert.That(orderedRulesets.SkipWhile(r => r.ShortName != "custom2").Skip(1).First().ShortName, Is.EqualTo("custom2"));
Assert.That(orderedRulesets.SkipWhile(r => r.ShortName != "custom3").Skip(1).First().ShortName, Is.EqualTo("custom3"));
}
}
}

View File

@ -26,12 +26,16 @@ namespace osu.Game.Tests.NonVisual
score.Statistics[HitResult.Good]++; score.Statistics[HitResult.Good]++;
score.Rank = ScoreRank.X; score.Rank = ScoreRank.X;
score.RealmUser.Username = "test";
Assert.That(scoreCopy.Statistics[HitResult.Good], Is.EqualTo(10)); Assert.That(scoreCopy.Statistics[HitResult.Good], Is.EqualTo(10));
Assert.That(score.Statistics[HitResult.Good], Is.EqualTo(11)); Assert.That(score.Statistics[HitResult.Good], Is.EqualTo(11));
Assert.That(scoreCopy.Rank, Is.EqualTo(ScoreRank.B)); Assert.That(scoreCopy.Rank, Is.EqualTo(ScoreRank.B));
Assert.That(score.Rank, Is.EqualTo(ScoreRank.X)); Assert.That(score.Rank, Is.EqualTo(ScoreRank.X));
Assert.That(scoreCopy.RealmUser.Username, Is.Empty);
Assert.That(score.RealmUser.Username, Is.EqualTo("test"));
} }
[Test] [Test]

View File

@ -61,7 +61,6 @@ namespace osu.Game.Tests.NonVisual.Skinning
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException(); public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException();
public ISample GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException(); public ISample GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException();
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotSupportedException(); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotSupportedException();
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
} }
private class TestAnimationTimeReference : IAnimationTimeReference private class TestAnimationTimeReference : IAnimationTimeReference

View File

@ -45,9 +45,9 @@ namespace osu.Game.Tests.Online
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, GameHost host) private void load(AudioManager audio, GameHost host)
{ {
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(rulesets = new RulesetStore(Realm));
Dependencies.CacheAs<BeatmapManager>(beatmaps = new TestBeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); Dependencies.CacheAs<BeatmapManager>(beatmaps = new TestBeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default));
Dependencies.CacheAs<BeatmapModelDownloader>(beatmapDownloader = new TestBeatmapModelDownloader(beatmaps, API, host)); Dependencies.CacheAs<BeatmapModelDownloader>(beatmapDownloader = new TestBeatmapModelDownloader(beatmaps, API));
} }
[SetUp] [SetUp]
@ -60,8 +60,8 @@ namespace osu.Game.Tests.Online
testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile); testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile);
testBeatmapSet = testBeatmapInfo.BeatmapSet; testBeatmapSet = testBeatmapInfo.BeatmapSet;
ContextFactory.Write(r => r.RemoveAll<BeatmapSetInfo>()); Realm.Write(r => r.RemoveAll<BeatmapSetInfo>());
ContextFactory.Write(r => r.RemoveAll<BeatmapInfo>()); Realm.Write(r => r.RemoveAll<BeatmapInfo>());
selectedItem.Value = new PlaylistItem selectedItem.Value = new PlaylistItem
{ {
@ -91,7 +91,7 @@ namespace osu.Game.Tests.Online
addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing); addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing);
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
AddUntilStep("wait for import", () => beatmaps.CurrentImportTask?.IsCompleted == true); AddUntilStep("wait for import", () => beatmaps.CurrentImport != null);
addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable);
} }
@ -164,39 +164,39 @@ namespace osu.Game.Tests.Online
{ {
public TaskCompletionSource<bool> AllowImport = new TaskCompletionSource<bool>(); public TaskCompletionSource<bool> AllowImport = new TaskCompletionSource<bool>();
public Task<ILive<BeatmapSetInfo>> CurrentImportTask { get; private set; } public Live<BeatmapSetInfo> CurrentImport { get; private set; }
public TestBeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null)
: base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) : base(storage, realm, rulesets, api, audioManager, resources, host, defaultBeatmap)
{ {
} }
protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue) protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue)
{ {
return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, onlineLookupQueue); return new TestBeatmapModelManager(this, storage, realm, onlineLookupQueue);
} }
internal class TestBeatmapModelManager : BeatmapModelManager internal class TestBeatmapModelManager : BeatmapModelManager
{ {
private readonly TestBeatmapManager testBeatmapManager; private readonly TestBeatmapManager testBeatmapManager;
public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, RealmContextFactory databaseContextFactory, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue)
: base(databaseContextFactory, storage, beatmapOnlineLookupQueue) : base(databaseAccess, storage, beatmapOnlineLookupQueue)
{ {
this.testBeatmapManager = testBeatmapManager; this.testBeatmapManager = testBeatmapManager;
} }
public override async Task<ILive<BeatmapSetInfo>> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) public override Live<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
{ {
await testBeatmapManager.AllowImport.Task.ConfigureAwait(false); testBeatmapManager.AllowImport.Task.WaitSafely();
return await (testBeatmapManager.CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false); return (testBeatmapManager.CurrentImport = base.Import(item, archive, lowPriority, cancellationToken));
} }
} }
} }
internal class TestBeatmapModelDownloader : BeatmapModelDownloader internal class TestBeatmapModelDownloader : BeatmapModelDownloader
{ {
public TestBeatmapModelDownloader(IModelImporter<BeatmapSetInfo> importer, IAPIProvider apiProvider, GameHost gameHost) public TestBeatmapModelDownloader(IModelImporter<BeatmapSetInfo> importer, IAPIProvider apiProvider)
: base(importer, apiProvider) : base(importer, apiProvider)
{ {
} }

View File

@ -80,7 +80,10 @@ namespace osu.Game.Tests.Resources
public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null) public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null)
{ {
int j = 0; int j = 0;
RulesetInfo getRuleset() => rulesets?[j++ % rulesets.Length] ?? new OsuRuleset().RulesetInfo;
rulesets ??= new[] { new OsuRuleset().RulesetInfo };
RulesetInfo getRuleset() => rulesets?[j++ % rulesets.Length];
int setId = Interlocked.Increment(ref importId); int setId = Interlocked.Increment(ref importId);

View File

@ -0,0 +1,3 @@
[Difficulty]
OverallDifficulty:1
ApproachRate:9

View File

@ -0,0 +1,3 @@
[Difficulty]
ApproachRate:9
OverallDifficulty:1

View File

@ -0,0 +1,2 @@
[Difficulty]
OverallDifficulty:1

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
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;
@ -25,7 +24,7 @@ namespace osu.Game.Tests.Scores.IO
public class ImportScoreTest : ImportTest public class ImportScoreTest : ImportTest
{ {
[Test] [Test]
public async Task TestBasicImport() public void TestBasicImport()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
@ -49,7 +48,7 @@ namespace osu.Game.Tests.Scores.IO
BeatmapInfo = beatmap.Beatmaps.First() BeatmapInfo = beatmap.Beatmaps.First()
}; };
var imported = await LoadScoreIntoOsu(osu, toImport); var imported = LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank); Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore); Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
@ -67,7 +66,7 @@ namespace osu.Game.Tests.Scores.IO
} }
[Test] [Test]
public async Task TestImportMods() public void TestImportMods()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
@ -85,7 +84,7 @@ namespace osu.Game.Tests.Scores.IO
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
}; };
var imported = await LoadScoreIntoOsu(osu, toImport); var imported = LoadScoreIntoOsu(osu, toImport);
Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock)); Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock));
Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime)); Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime));
@ -98,7 +97,7 @@ namespace osu.Game.Tests.Scores.IO
} }
[Test] [Test]
public async Task TestImportStatistics() public void TestImportStatistics()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
@ -120,7 +119,7 @@ namespace osu.Game.Tests.Scores.IO
} }
}; };
var imported = await LoadScoreIntoOsu(osu, toImport); var imported = LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]); Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]);
Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]); Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]);
@ -133,7 +132,7 @@ namespace osu.Game.Tests.Scores.IO
} }
[Test] [Test]
public async Task TestOnlineScoreIsAvailableLocally() public void TestOnlineScoreIsAvailableLocally()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{ {
@ -143,7 +142,7 @@ namespace osu.Game.Tests.Scores.IO
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
await LoadScoreIntoOsu(osu, new ScoreInfo LoadScoreIntoOsu(osu, new ScoreInfo
{ {
User = new APIUser { Username = "Test user" }, User = new APIUser { Username = "Test user" },
BeatmapInfo = beatmap.Beatmaps.First(), BeatmapInfo = beatmap.Beatmaps.First(),
@ -168,13 +167,14 @@ namespace osu.Game.Tests.Scores.IO
} }
} }
public static async Task<ScoreInfo> LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) public static ScoreInfo LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
{ {
// clone to avoid attaching the input score to realm. // clone to avoid attaching the input score to realm.
score = score.DeepClone(); score = score.DeepClone();
var scoreManager = osu.Dependencies.Get<ScoreManager>(); var scoreManager = osu.Dependencies.Get<ScoreManager>();
await scoreManager.Import(score, archive);
scoreManager.Import(score, archive);
return scoreManager.Query(_ => true); return scoreManager.Query(_ => true);
} }

View File

@ -235,7 +235,7 @@ namespace osu.Game.Tests.Skins.IO
#endregion #endregion
private void assertCorrectMetadata(ILive<SkinInfo> import1, string name, string creator, OsuGameBase osu) private void assertCorrectMetadata(Live<SkinInfo> import1, string name, string creator, OsuGameBase osu)
{ {
import1.PerformRead(i => import1.PerformRead(i =>
{ {
@ -250,7 +250,7 @@ namespace osu.Game.Tests.Skins.IO
}); });
} }
private void assertImportedBoth(ILive<SkinInfo> import1, ILive<SkinInfo> import2) private void assertImportedBoth(Live<SkinInfo> import1, Live<SkinInfo> import2)
{ {
import1.PerformRead(i1 => import2.PerformRead(i2 => import1.PerformRead(i1 => import2.PerformRead(i2 =>
{ {
@ -260,7 +260,7 @@ namespace osu.Game.Tests.Skins.IO
})); }));
} }
private void assertImportedOnce(ILive<SkinInfo> import1, ILive<SkinInfo> import2) private void assertImportedOnce(Live<SkinInfo> import1, Live<SkinInfo> import2)
{ {
import1.PerformRead(i1 => import2.PerformRead(i2 => import1.PerformRead(i1 => import2.PerformRead(i2 =>
{ {
@ -334,7 +334,7 @@ namespace osu.Game.Tests.Skins.IO
} }
} }
private async Task<ILive<SkinInfo>> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null) private async Task<Live<SkinInfo>> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null)
{ {
var skinManager = osu.Dependencies.Get<SkinManager>(); var skinManager = osu.Dependencies.Get<SkinManager>();
return await skinManager.Import(archive); return await skinManager.Import(archive);

View File

@ -47,10 +47,10 @@ namespace osu.Game.Tests.Visual.Background
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load(GameHost host, AudioManager audio)
{ {
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(rulesets = new RulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new OsuConfigManager(LocalStorage)); Dependencies.Cache(new OsuConfigManager(LocalStorage));
Dependencies.Cache(ContextFactory); Dependencies.Cache(Realm);
manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();

View File

@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.Collections
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host) private void load(GameHost host)
{ {
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(rulesets = new RulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, Resources, host, Beatmap.Default)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(ContextFactory); Dependencies.Cache(Realm);
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();

View File

@ -13,6 +13,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
@ -37,11 +38,8 @@ namespace osu.Game.Tests.Visual.Editing
base.SetUpSteps(); base.SetUpSteps();
} }
protected override void LoadEditor() protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{ => beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First());
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First());
base.LoadEditor();
}
[Test] [Test]
public void TestBasicSwitch() public void TestBasicSwitch()
@ -84,8 +82,8 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("set target difficulty", () => AddStep("set target difficulty", () =>
{ {
targetDifficulty = sameRuleset targetDifficulty = sameRuleset
? importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo) && beatmap.RulesetID == Beatmap.Value.BeatmapInfo.RulesetID) ? importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo) && beatmap.Ruleset.ShortName == Beatmap.Value.BeatmapInfo.Ruleset.ShortName)
: importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo) && beatmap.RulesetID != Beatmap.Value.BeatmapInfo.RulesetID); : importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo) && beatmap.Ruleset.ShortName != Beatmap.Value.BeatmapInfo.Ruleset.ShortName);
}); });
switchToDifficulty(() => targetDifficulty); switchToDifficulty(() => targetDifficulty);
confirmEditingBeatmap(() => targetDifficulty); confirmEditingBeatmap(() => targetDifficulty);

View File

@ -13,6 +13,7 @@ using osu.Game.Rulesets;
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.Setup; using osu.Game.Screens.Edit.Setup;
using osu.Game.Storyboards;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using SharpCompress.Archives; using SharpCompress.Archives;
using SharpCompress.Archives.Zip; using SharpCompress.Archives.Zip;
@ -39,11 +40,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString()); AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString());
} }
protected override void LoadEditor() protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new DummyWorkingBeatmap(Audio, null);
{
Beatmap.Value = new DummyWorkingBeatmap(Audio, null);
base.LoadEditor();
}
[Test] [Test]
public void TestCreateNewBeatmap() public void TestCreateNewBeatmap()
@ -93,5 +90,100 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000); AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
} }
[Test]
public void TestCreateNewDifficulty()
{
string firstDifficultyName = Guid.NewGuid().ToString();
string secondDifficultyName = Guid.NewGuid().ToString();
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName);
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () =>
{
var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == firstDifficultyName);
var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID);
return beatmap != null
&& beatmap.DifficultyName == firstDifficultyName
&& set != null
&& set.PerformRead(s => s.Beatmaps.Single().ID == beatmap.ID);
});
AddAssert("can save again", () => Editor.Save());
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
AddUntilStep("wait for created", () =>
{
string difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != firstDifficultyName;
});
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName);
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () =>
{
var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName);
var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID);
return beatmap != null
&& beatmap.DifficultyName == secondDifficultyName
&& set != null
&& set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName));
});
}
[Test]
public void TestCreateNewBeatmapFailsWithBlankNamedDifficulties()
{
Guid setId = Guid.Empty;
AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID);
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () =>
{
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1);
});
AddStep("try to create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
AddAssert("beatmap set unchanged", () =>
{
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1);
});
}
[Test]
public void TestCreateNewBeatmapFailsWithSameNamedDifficulties()
{
Guid setId = Guid.Empty;
const string duplicate_difficulty_name = "duplicate";
AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID);
AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name);
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () =>
{
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1);
});
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
AddUntilStep("wait for created", () =>
{
string difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != duplicate_difficulty_name;
});
AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name);
AddStep("try to save beatmap", () => Editor.Save());
AddAssert("beatmap set not corrupted", () =>
{
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
// the difficulty was already created at the point of the switch.
// what we want to check is that both difficulties do not use the same file.
return set != null && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Files.Count == 2);
});
}
} }
} }

View File

@ -3,92 +3,139 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Input; using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
{ {
public class TestSceneEditorSaving : OsuGameTestScene public class TestSceneEditorSaving : EditorSavingTestScene
{ {
private Editor editor => Game.ChildrenOfType<Editor>().FirstOrDefault();
private EditorBeatmap editorBeatmap => (EditorBeatmap)editor.Dependencies.Get(typeof(EditorBeatmap));
/// <summary>
/// Tests the general expected flow of creating a new beatmap, saving it, then loading it back from song select.
/// </summary>
[Test] [Test]
public void TestNewBeatmapSaveThenLoad() public void TestMetadata()
{ {
AddStep("set default beatmap", () => Game.Beatmap.SetDefault());
PushAndConfirm(() => new EditorLoader());
AddUntilStep("wait for editor load", () => editor?.IsLoaded == true);
AddUntilStep("wait for metadata screen load", () => editor.ChildrenOfType<MetadataSection>().FirstOrDefault()?.IsLoaded == true);
// We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten.
AddStep("Enter compose mode", () => InputManager.Key(Key.F1));
AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true);
AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7);
AddStep("Set artist and title", () => AddStep("Set artist and title", () =>
{ {
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist"; EditorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
editorBeatmap.BeatmapInfo.Metadata.Title = "title"; EditorBeatmap.BeatmapInfo.Metadata.Title = "title";
}); });
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty"); AddStep("Set author", () => EditorBeatmap.BeatmapInfo.Metadata.Author.Username = "author");
AddStep("Set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = "difficulty");
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); SaveEditor();
AddAssert("Beatmap has correct metadata", () => EditorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && EditorBeatmap.BeatmapInfo.Metadata.Title == "title");
AddAssert("Beatmap has correct author", () => EditorBeatmap.BeatmapInfo.Metadata.Author.Username == "author");
AddAssert("Beatmap has correct difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
AddAssert("Beatmap has correct .osu file path", () => EditorBeatmap.BeatmapInfo.Path == "artist - title (author) [difficulty].osu");
ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct metadata", () => EditorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && EditorBeatmap.BeatmapInfo.Metadata.Title == "title");
AddAssert("Beatmap still has correct author", () => EditorBeatmap.BeatmapInfo.Metadata.Author.Username == "author");
AddAssert("Beatmap still has correct difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
AddAssert("Beatmap still has correct .osu file path", () => EditorBeatmap.BeatmapInfo.Path == "artist - title (author) [difficulty].osu");
}
[Test]
public void TestConfiguration()
{
double originalTimelineZoom = 0;
double changedTimelineZoom = 0;
AddStep("Set beat divisor", () => Editor.Dependencies.Get<BindableBeatDivisor>().Value = 16);
AddStep("Set timeline zoom", () =>
{
originalTimelineZoom = EditorBeatmap.BeatmapInfo.TimelineZoom;
var timeline = Editor.ChildrenOfType<Timeline>().Single();
InputManager.MoveMouseTo(timeline);
InputManager.PressKey(Key.AltLeft);
InputManager.ScrollVerticalBy(15f);
InputManager.ReleaseKey(Key.AltLeft);
});
AddAssert("Ensure timeline zoom changed", () =>
{
changedTimelineZoom = EditorBeatmap.BeatmapInfo.TimelineZoom;
return !Precision.AlmostEquals(changedTimelineZoom, originalTimelineZoom);
});
SaveEditor();
AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16);
AddAssert("Beatmap has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom);
ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16);
AddAssert("Beatmap still has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom);
}
[Test]
public void TestDifficulty()
{
AddStep("Set overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty = 7);
SaveEditor();
AddAssert("Beatmap has correct overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty == 7);
ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty == 7);
}
[Test]
public void TestHitObjectPlacement()
{
AddStep("Add timing point", () => EditorBeatmap.ControlPointInfo.Add(500, new TimingControlPoint()));
AddStep("Change to placement mode", () => InputManager.Key(Key.Number2)); AddStep("Change to placement mode", () => InputManager.Key(Key.Number2));
AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left)); AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left));
checkMutations(); SaveEditor();
AddAssert("Beatmap has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500);
// After placement these must be non-default as defaults are read-only. // After placement these must be non-default as defaults are read-only.
AddAssert("Placed object has non-default control points", () => AddAssert("Placed object has non-default control points", () =>
editorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT && EditorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT &&
editorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT); EditorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT);
AddStep("Save", () => InputManager.Keys(PlatformAction.Save)); ReloadEditorToSameBeatmap();
checkMutations(); AddAssert("Beatmap still has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500);
AddStep("Exit", () => InputManager.Key(Key.Escape)); // After placement these must be non-default as defaults are read-only.
AddAssert("Placed object still has non-default control points", () =>
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); EditorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT &&
EditorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT);
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new PlaySongSelect());
AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded);
AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault);
AddStep("Open options", () => InputManager.Key(Key.F3));
AddStep("Enter editor", () => InputManager.Key(Key.Number5));
AddUntilStep("Wait for editor load", () => editor != null);
checkMutations();
} }
private void checkMutations() [Test]
public void TestExitWithoutSaveFromExistingBeatmap()
{ {
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); const string tags_to_save = "these tags will be saved";
AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7); const string tags_to_discard = "these tags should be discarded";
AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty"); AddStep("Set tags", () => EditorBeatmap.BeatmapInfo.Metadata.Tags = tags_to_save);
SaveEditor();
AddAssert("Tags saved correctly", () => EditorBeatmap.BeatmapInfo.Metadata.Tags == tags_to_save);
ReloadEditorToSameBeatmap();
AddAssert("Tags saved correctly", () => EditorBeatmap.BeatmapInfo.Metadata.Tags == tags_to_save);
AddStep("Set tags again", () => EditorBeatmap.BeatmapInfo.Metadata.Tags = tags_to_discard);
AddStep("Exit editor", () => Editor.Exit());
AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddAssert("Tags reverted correctly", () => Game.Beatmap.Value.BeatmapInfo.Metadata.Tags == tags_to_save);
} }
} }
} }

View File

@ -17,6 +17,7 @@ using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input; using osuTK.Input;
@ -43,9 +44,11 @@ namespace osu.Game.Tests.Visual.Editing
base.SetUpSteps(); base.SetUpSteps();
} }
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
=> beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0));
protected override void LoadEditor() protected override void LoadEditor()
{ {
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.RulesetID == 0));
SelectedMods.Value = new[] { new ModCinema() }; SelectedMods.Value = new[] { new ModCinema() };
base.LoadEditor(); base.LoadEditor();
} }
@ -67,7 +70,11 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddUntilStep("background has correct params", () => AddUntilStep("background has correct params", () =>
{ {
var background = this.ChildrenOfType<BackgroundScreenBeatmap>().Single(); // the test gameplay player's beatmap may be the "same" beatmap as the one being edited, *but* the `BeatmapInfo` references may differ
// due to the beatmap refetch logic ran on editor suspend.
// this test cares about checking the background belonging to the editor specifically, so check that using reference equality
// (as `.Equals()` cannot discern between the two, as they technically share the same database GUID).
var background = this.ChildrenOfType<BackgroundScreenBeatmap>().Single(b => ReferenceEquals(b.Beatmap.BeatmapInfo, EditorBeatmap.BeatmapInfo));
return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0; return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0;
}); });
AddAssert("no mods selected", () => SelectedMods.Value.Count == 0); AddAssert("no mods selected", () => SelectedMods.Value.Count == 0);
@ -96,7 +103,11 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddUntilStep("background has correct params", () => AddUntilStep("background has correct params", () =>
{ {
var background = this.ChildrenOfType<BackgroundScreenBeatmap>().Single(); // the test gameplay player's beatmap may be the "same" beatmap as the one being edited, *but* the `BeatmapInfo` references may differ
// due to the beatmap refetch logic ran on editor suspend.
// this test cares about checking the background belonging to the editor specifically, so check that using reference equality
// (as `.Equals()` cannot discern between the two, as they technically share the same database GUID).
var background = this.ChildrenOfType<BackgroundScreenBeatmap>().Single(b => ReferenceEquals(b.Beatmap.BeatmapInfo, EditorBeatmap.BeatmapInfo));
return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0; return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0;
}); });

View File

@ -0,0 +1,88 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit.Timing;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneLabelledTimeSignature : OsuManualInputManagerTestScene
{
private LabelledTimeSignature timeSignature;
private void createLabelledTimeSignature(TimeSignature initial) => AddStep("create labelled time signature", () =>
{
Child = timeSignature = new LabelledTimeSignature
{
Label = "Time Signature",
RelativeSizeAxes = Axes.None,
Width = 400,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = { Value = initial }
};
});
private OsuTextBox numeratorTextBox => timeSignature.ChildrenOfType<OsuTextBox>().Single();
[Test]
public void TestInitialValue()
{
createLabelledTimeSignature(TimeSignature.SimpleTriple);
AddAssert("current is 3/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleTriple));
}
[Test]
public void TestChangeViaCurrent()
{
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("set current to 5/4", () => timeSignature.Current.Value = new TimeSignature(5));
AddAssert("current is 5/4", () => timeSignature.Current.Value.Equals(new TimeSignature(5)));
AddAssert("numerator is 5", () => numeratorTextBox.Current.Value == "5");
AddStep("set current to 3/4", () => timeSignature.Current.Value = TimeSignature.SimpleTriple);
AddAssert("current is 3/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleTriple));
AddAssert("numerator is 3", () => numeratorTextBox.Current.Value == "3");
}
[Test]
public void TestChangeNumerator()
{
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7");
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("drop focus", () => InputManager.ChangeFocus(null));
AddAssert("current is 7/4", () => timeSignature.Current.Value.Equals(new TimeSignature(7)));
}
[Test]
public void TestInvalidChangeRollbackOnCommit()
{
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0");
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("drop focus", () => InputManager.ChangeFocus(null));
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4");
}
}
}

View File

@ -47,25 +47,25 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
{ {
new HitCircle { StartTime = 100 }, new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 200, Position = new Vector2(100) }, new HitCircle { StartTime = 1000, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(200) }, new HitCircle { StartTime = 1500, Position = new Vector2(200) },
new HitCircle { StartTime = 400, Position = new Vector2(300) }, new HitCircle { StartTime = 2000, Position = new Vector2(300) },
})); }));
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
AddStep("nudge forwards", () => InputManager.Key(Key.K)); AddStep("nudge forwards", () => InputManager.Key(Key.K));
AddAssert("objects moved forwards in time", () => addedObjects[0].StartTime > 100); AddAssert("objects moved forwards in time", () => addedObjects[0].StartTime > 500);
AddStep("nudge backwards", () => InputManager.Key(Key.J)); AddStep("nudge backwards", () => InputManager.Key(Key.J));
AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100); AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 500);
} }
[Test] [Test]
public void TestBasicSelect() public void TestBasicSelect()
{ {
var addedObject = new HitCircle { StartTime = 100 }; var addedObject = new HitCircle { StartTime = 500 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
moveMouseToObject(() => addedObject); moveMouseToObject(() => addedObject);
@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Editing
var addedObject2 = new HitCircle var addedObject2 = new HitCircle
{ {
StartTime = 200, StartTime = 1000,
Position = new Vector2(100), Position = new Vector2(100),
}; };
@ -92,10 +92,10 @@ namespace osu.Game.Tests.Visual.Editing
{ {
var addedObjects = new[] var addedObjects = new[]
{ {
new HitCircle { StartTime = 100 }, new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 200, Position = new Vector2(100) }, new HitCircle { StartTime = 1000, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(200) }, new HitCircle { StartTime = 1500, Position = new Vector2(200) },
new HitCircle { StartTime = 400, Position = new Vector2(300) }, new HitCircle { StartTime = 2000, Position = new Vector2(300) },
}; };
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
@ -125,7 +125,7 @@ namespace osu.Game.Tests.Visual.Editing
[Test] [Test]
public void TestBasicDeselect() public void TestBasicDeselect()
{ {
var addedObject = new HitCircle { StartTime = 100 }; var addedObject = new HitCircle { StartTime = 500 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
moveMouseToObject(() => addedObject); moveMouseToObject(() => addedObject);
@ -166,11 +166,11 @@ namespace osu.Game.Tests.Visual.Editing
{ {
var addedObjects = new[] var addedObjects = new[]
{ {
new HitCircle { StartTime = 100 }, new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 200, Position = new Vector2(100) }, new HitCircle { StartTime = 1000, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(200) }, new HitCircle { StartTime = 1500, Position = new Vector2(200) },
new HitCircle { StartTime = 400, Position = new Vector2(300) }, new HitCircle { StartTime = 2000, Position = new Vector2(300) },
new HitCircle { StartTime = 500, Position = new Vector2(400) }, new HitCircle { StartTime = 2500, Position = new Vector2(400) },
}; };
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
@ -236,10 +236,10 @@ namespace osu.Game.Tests.Visual.Editing
{ {
var addedObjects = new[] var addedObjects = new[]
{ {
new HitCircle { StartTime = 100 }, new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 200, Position = new Vector2(100) }, new HitCircle { StartTime = 1000, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(200) }, new HitCircle { StartTime = 1500, Position = new Vector2(200) },
new HitCircle { StartTime = 400, Position = new Vector2(300) }, new HitCircle { StartTime = 2000, Position = new Vector2(300) },
}; };
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));

View File

@ -3,9 +3,9 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -19,28 +19,35 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override double TimePerAction => 100; // required for the early exit test, since hold-to-confirm delay is 200ms protected override double TimePerAction => 100; // required for the early exit test, since hold-to-confirm delay is 200ms
[BackgroundDependencyLoader] private HoldForMenuButton holdForMenuButton;
private void load()
[SetUpSteps]
public void SetUpSteps()
{ {
HoldForMenuButton holdForMenuButton; AddStep("create button", () =>
Add(holdForMenuButton = new HoldForMenuButton
{ {
Origin = Anchor.BottomRight, exitAction = false;
Anchor = Anchor.BottomRight,
Action = () => exitAction = true Child = holdForMenuButton = new HoldForMenuButton
{
Scale = new Vector2(2),
Origin = Anchor.CentreRight,
Anchor = Anchor.CentreRight,
Action = () => exitAction = true
};
}); });
}
var text = holdForMenuButton.Children.OfType<SpriteText>().First(); [Test]
public void TestMovementAndTrigger()
{
AddStep("Trigger text fade in", () => InputManager.MoveMouseTo(holdForMenuButton)); AddStep("Trigger text fade in", () => InputManager.MoveMouseTo(holdForMenuButton));
AddUntilStep("Text visible", () => text.IsPresent && !exitAction); AddUntilStep("Text visible", () => getSpriteText().IsPresent && !exitAction);
AddStep("Trigger text fade out", () => InputManager.MoveMouseTo(Vector2.One)); AddStep("Trigger text fade out", () => InputManager.MoveMouseTo(Vector2.One));
AddUntilStep("Text is not visible", () => !text.IsPresent && !exitAction); AddUntilStep("Text is not visible", () => !getSpriteText().IsPresent && !exitAction);
AddStep("Trigger exit action", () => AddStep("Trigger exit action", () =>
{ {
exitAction = false;
InputManager.MoveMouseTo(holdForMenuButton); InputManager.MoveMouseTo(holdForMenuButton);
InputManager.PressButton(MouseButton.Left); InputManager.PressButton(MouseButton.Left);
}); });
@ -50,6 +57,17 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Trigger exit action", () => InputManager.PressButton(MouseButton.Left)); AddStep("Trigger exit action", () => InputManager.PressButton(MouseButton.Left));
AddUntilStep($"{nameof(holdForMenuButton.Action)} was triggered", () => exitAction); AddUntilStep($"{nameof(holdForMenuButton.Action)} was triggered", () => exitAction);
AddStep("Release", () => InputManager.ReleaseButton(MouseButton.Left));
} }
[Test]
public void TestFadeOnNoInput()
{
AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.One));
AddUntilStep("wait for text fade out", () => !getSpriteText().IsPresent);
AddUntilStep("wait for button fade out", () => holdForMenuButton.Alpha < 0.1f);
}
private SpriteText getSpriteText() => holdForMenuButton.Children.OfType<SpriteText>().First();
} }
} }

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