diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 6444127594..985fc09df3 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -27,10 +27,10 @@
]
},
"ppy.localisationanalyser.tools": {
- "version": "2021.725.0",
+ "version": "2021.1210.0",
"commands": [
"localisation"
]
}
}
-}
\ No newline at end of file
+}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 68f8ef51ef..3c52802cf6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -77,10 +77,6 @@ jobs:
run: msbuild osu.Android/osu.Android.csproj /restore /p:Configuration=Debug
build-only-ios:
- # While this workflow technically *can* run, it fails as iOS builds are blocked by multiple issues.
- # See https://github.com/ppy/osu-framework/issues/4677 for the details.
- # The job can be unblocked once those issues are resolved and game deployments can happen again.
- if: false
name: Build only (iOS)
runs-on: macos-latest
timeout-minutes: 60
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e14be20642..ae2bdd2e82 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,6 @@
# Contributing Guidelines
-Thank you for showing interest in the development of osu!lazer! We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.
+Thank you for showing interest in the development of osu!. We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.
These are not "official rules" *per se*, but following them will help everyone deal with things in the most efficient manner.
@@ -32,7 +32,7 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in
* **Provide more information when asked to do so.**
- Sometimes when a bug is more elusive or complicated, none of the information listed above will pinpoint a concrete cause of the problem. In this case we will most likely ask you for additional info, such as a Windows Event Log dump or a copy of your local lazer database (`client.db`). Providing that information is beneficial to both parties - we can track down the problem better, and hopefully fix it for you at some point once we know where it is!
+ Sometimes when a bug is more elusive or complicated, none of the information listed above will pinpoint a concrete cause of the problem. In this case we will most likely ask you for additional info, such as a Windows Event Log dump or a copy of your local osu! database (`client.db`). Providing that information is beneficial to both parties - we can track down the problem better, and hopefully fix it for you at some point once we know where it is!
* **When submitting a feature proposal, please describe it in the most understandable way you can.**
@@ -54,7 +54,7 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in
We also welcome pull requests from unaffiliated contributors. The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues that you can work on; we also mark issues that we think would be good for newcomers with the [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label.
-However, do keep in mind that the core team is committed to bringing osu!lazer up to par with stable first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management).
+However, do keep in mind that the core team is committed to bringing osu!(lazer) up to par with osu!(stable) first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management).
Here are some key things to note before jumping in:
@@ -128,7 +128,7 @@ Here are some key things to note before jumping in:
* **Don't mistake criticism of code for criticism of your person.**
- As mentioned before, we are highly committed to quality when it comes to the lazer project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience, and don't treat it as a personal attack.
+ As mentioned before, we are highly committed to quality when it comes to the osu! project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience, and don't treat it as a personal attack.
* **Feel free to reach out for help.**
diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index b72803482d..c567adc0ae 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -10,3 +10,6 @@ T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal
T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast() instead.
+M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection,NotificationCallbackDelegate) instead.
+M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable,NotificationCallbackDelegate) instead.
+M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList,NotificationCallbackDelegate) instead.
diff --git a/README.md b/README.md
index 786ce2589d..f18c5e76f9 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
A free-to-win rhythm game. Rhythm is just a *click* away!
-The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the codename "*lazer*". As in sharper than cutting-edge.
+The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge.
## Status
@@ -48,7 +48,7 @@ You can see some examples of custom rulesets by visiting the [custom ruleset dir
Please make sure you have the following prerequisites:
-- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) or higher installed.
+- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) installed.
- When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.
diff --git a/osu.Android.props b/osu.Android.props
index eff0eed278..1532d4ce23 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationRequest.cs b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationRequest.cs
new file mode 100644
index 0000000000..d6ef390a8f
--- /dev/null
+++ b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationRequest.cs
@@ -0,0 +1,18 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Desktop.LegacyIpc
+{
+ ///
+ /// A difficulty calculation request from the legacy client.
+ ///
+ ///
+ /// Synchronise any changes with osu!stable.
+ ///
+ public class LegacyIpcDifficultyCalculationRequest
+ {
+ public string BeatmapFile { get; set; }
+ public int RulesetId { get; set; }
+ public int Mods { get; set; }
+ }
+}
diff --git a/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationResponse.cs b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationResponse.cs
new file mode 100644
index 0000000000..7b9fae5797
--- /dev/null
+++ b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationResponse.cs
@@ -0,0 +1,16 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Desktop.LegacyIpc
+{
+ ///
+ /// A difficulty calculation response returned to the legacy client.
+ ///
+ ///
+ /// Synchronise any changes with osu!stable.
+ ///
+ public class LegacyIpcDifficultyCalculationResponse
+ {
+ public double StarRating { get; set; }
+ }
+}
diff --git a/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs
new file mode 100644
index 0000000000..0fa60e2068
--- /dev/null
+++ b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs
@@ -0,0 +1,53 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Platform;
+using Newtonsoft.Json.Linq;
+
+namespace osu.Desktop.LegacyIpc
+{
+ ///
+ /// An that can be used to communicate to and from legacy clients.
+ ///
+ /// In order to deserialise types at either end, types must be serialised as their ,
+ /// however this cannot be done since osu!stable and osu!lazer live in two different assemblies.
+ ///
+ /// To get around this, this class exists which serialises a payload () as an type,
+ /// which can be deserialised at either end because it is part of the core library (mscorlib / System.Private.CorLib).
+ /// The payload contains the data to be sent over the IPC channel.
+ ///
+ /// At either end, Json.NET deserialises the payload into a which is manually converted back into the expected type,
+ /// which then further contains another representing the data sent over the IPC channel whose type can likewise be lazily matched through
+ /// .
+ ///
+ ///
+ ///
+ /// Synchronise any changes with osu-stable.
+ ///
+ public class LegacyIpcMessage : IpcMessage
+ {
+ public LegacyIpcMessage()
+ {
+ // Types/assemblies are not inter-compatible, so always serialise/deserialise into objects.
+ base.Type = typeof(object).FullName;
+ }
+
+ public new string Type => base.Type; // Hide setter.
+
+ public new object Value
+ {
+ get => base.Value;
+ set => base.Value = new Data
+ {
+ MessageType = value.GetType().Name,
+ MessageData = value
+ };
+ }
+
+ public class Data
+ {
+ public string MessageType { get; set; }
+ public object MessageData { get; set; }
+ }
+ }
+}
diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs
new file mode 100644
index 0000000000..97a4c57bf0
--- /dev/null
+++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs
@@ -0,0 +1,121 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using Newtonsoft.Json.Linq;
+using osu.Framework.Logging;
+using osu.Framework.Platform;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Legacy;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
+using osu.Game.Rulesets.Mania;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Taiko;
+
+#nullable enable
+
+namespace osu.Desktop.LegacyIpc
+{
+ ///
+ /// Provides IPC to legacy osu! clients.
+ ///
+ public class LegacyTcpIpcProvider : TcpIpcProvider
+ {
+ private static readonly Logger logger = Logger.GetLogger("legacy-ipc");
+
+ public LegacyTcpIpcProvider()
+ : base(45357)
+ {
+ MessageReceived += msg =>
+ {
+ try
+ {
+ logger.Add("Processing legacy IPC message...");
+ logger.Add($" {msg.Value}", LogLevel.Debug);
+
+ // See explanation in LegacyIpcMessage for why this is done this way.
+ var legacyData = ((JObject)msg.Value).ToObject();
+ object value = parseObject((JObject)legacyData!.MessageData, legacyData.MessageType);
+
+ return new LegacyIpcMessage
+ {
+ Value = onLegacyIpcMessageReceived(value)
+ };
+ }
+ catch (Exception ex)
+ {
+ logger.Add($"Processing IPC message failed: {msg.Value}", exception: ex);
+ return null;
+ }
+ };
+ }
+
+ private object parseObject(JObject value, string type)
+ {
+ switch (type)
+ {
+ case nameof(LegacyIpcDifficultyCalculationRequest):
+ return value.ToObject()
+ ?? throw new InvalidOperationException($"Failed to parse request {value}");
+
+ case nameof(LegacyIpcDifficultyCalculationResponse):
+ return value.ToObject()
+ ?? throw new InvalidOperationException($"Failed to parse request {value}");
+
+ default:
+ throw new ArgumentException($"Unsupported object type {type}");
+ }
+ }
+
+ private object onLegacyIpcMessageReceived(object message)
+ {
+ switch (message)
+ {
+ case LegacyIpcDifficultyCalculationRequest req:
+ try
+ {
+ var ruleset = getLegacyRulesetFromID(req.RulesetId);
+
+ Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray();
+ WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile, _ => ruleset);
+
+ return new LegacyIpcDifficultyCalculationResponse
+ {
+ StarRating = ruleset.CreateDifficultyCalculator(beatmap).Calculate(mods).StarRating
+ };
+ }
+ catch
+ {
+ return new LegacyIpcDifficultyCalculationResponse();
+ }
+
+ default:
+ throw new ArgumentException($"Unsupported message type {message}");
+ }
+ }
+
+ private static Ruleset getLegacyRulesetFromID(int rulesetId)
+ {
+ switch (rulesetId)
+ {
+ case 0:
+ return new OsuRuleset();
+
+ case 1:
+ return new TaikoRuleset();
+
+ case 2:
+ return new CatchRuleset();
+
+ case 3:
+ return new ManiaRuleset();
+
+ default:
+ throw new ArgumentException("Invalid ruleset id");
+ }
+ }
+ }
+}
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index 645ea66654..b234207848 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -70,7 +70,9 @@ namespace osu.Desktop
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
return stableInstallPath;
}
- catch { }
+ catch
+ {
+ }
}
stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
@@ -113,7 +115,7 @@ namespace osu.Desktop
base.LoadComplete();
if (!noVersionOverlay)
- LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add);
+ LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add);
LoadComponentAsync(new DiscordRichPresence(), Add);
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index 898f7d5105..7ec7d53a7e 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -5,6 +5,7 @@ using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using osu.Desktop.LegacyIpc;
using osu.Framework;
using osu.Framework.Development;
using osu.Framework.Logging;
@@ -18,8 +19,10 @@ namespace osu.Desktop
{
private const string base_game_name = @"osu";
+ private static LegacyTcpIpcProvider legacyIpc;
+
[STAThread]
- public static int Main(string[] args)
+ public static void Main(string[] args)
{
// Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory;
@@ -69,14 +72,28 @@ namespace osu.Desktop
throw new TimeoutException(@"IPC took too long to send");
}
- return 0;
+ return;
}
// we want to allow multiple instances to be started when in debug.
if (!DebugUtils.IsDebugBuild)
{
Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error);
- return 0;
+ return;
+ }
+ }
+
+ if (host.IsPrimaryInstance)
+ {
+ try
+ {
+ Logger.Log("Starting legacy IPC provider...");
+ legacyIpc = new LegacyTcpIpcProvider();
+ legacyIpc.Bind();
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, "Failed to start legacy IPC provider");
}
}
@@ -84,8 +101,6 @@ namespace osu.Desktop
host.Run(new TournamentGame());
else
host.Run(new OsuGameDesktop(args));
-
- return 0;
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist
index 5115746cbb..3ba1886d98 100644
--- a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist
@@ -32,5 +32,7 @@
XSAppIconAssets
Assets.xcassets/AppIcon.appiconset
+ CADisableMinimumFrameDurationOnPhone
+
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
index 33fdcdaf1e..be1885cfa6 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
@@ -27,6 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase("hardrock-repeat-slider", new[] { typeof(CatchModHardRock) })]
[TestCase("hardrock-spinner", new[] { typeof(CatchModHardRock) })]
[TestCase("right-bound-hr-offset", new[] { typeof(CatchModHardRock) })]
+ [TestCase("basic-hyperdash")]
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
@@ -70,6 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests
HitObject = hitObject;
startTime = 0;
position = 0;
+ hyperDash = false;
}
private double startTime;
@@ -88,8 +90,17 @@ namespace osu.Game.Rulesets.Catch.Tests
set => position = value;
}
+ private bool hyperDash;
+
+ public bool HyperDash
+ {
+ get => (HitObject as PalpableCatchHitObject)?.HyperDash ?? hyperDash;
+ set => hyperDash = value;
+ }
+
public bool Equals(ConvertValue other)
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
- && Precision.AlmostEquals(Position, other.Position, conversion_lenience);
+ && Precision.AlmostEquals(Position, other.Position, conversion_lenience)
+ && HyperDash == other.HyperDash;
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
index 70b2c8c82a..14a4d02396 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private Drawable setupSkinHierarchy(Drawable child, ISkin skin)
{
- var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info));
+ var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.CreateInfo()));
var testSkinProvider = new SkinProvidingContainer(skin);
var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider));
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json
index b65d54a565..07ceb199bd 100644
--- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json
@@ -3,135 +3,168 @@
"StartTime": 500,
"Objects": [{
"StartTime": 500,
- "Position": 96
+ "Position": 96,
+ "HyperDash": false
},
{
"StartTime": 562,
- "Position": 100.84
+ "Position": 100.84,
+ "HyperDash": false
},
{
"StartTime": 625,
- "Position": 125
+ "Position": 125,
+ "HyperDash": false
},
{
"StartTime": 687,
- "Position": 152.84
+ "Position": 152.84,
+ "HyperDash": false
},
{
"StartTime": 750,
- "Position": 191
+ "Position": 191,
+ "HyperDash": false
},
{
"StartTime": 812,
- "Position": 212.84
+ "Position": 212.84,
+ "HyperDash": false
},
{
"StartTime": 875,
- "Position": 217
+ "Position": 217,
+ "HyperDash": false
},
{
"StartTime": 937,
- "Position": 234.84
+ "Position": 234.84,
+ "HyperDash": false
},
{
"StartTime": 1000,
- "Position": 256
+ "Position": 256,
+ "HyperDash": false
},
{
"StartTime": 1062,
- "Position": 267.84
+ "Position": 267.84,
+ "HyperDash": false
},
{
"StartTime": 1125,
- "Position": 284
+ "Position": 284,
+ "HyperDash": false
},
{
"StartTime": 1187,
- "Position": 311.84
+ "Position": 311.84,
+ "HyperDash": false
},
{
"StartTime": 1250,
- "Position": 350
+ "Position": 350,
+ "HyperDash": false
},
{
"StartTime": 1312,
- "Position": 359.84
+ "Position": 359.84,
+ "HyperDash": false
},
{
"StartTime": 1375,
- "Position": 367
+ "Position": 367,
+ "HyperDash": false
},
{
"StartTime": 1437,
- "Position": 400.84
+ "Position": 400.84,
+ "HyperDash": false
},
{
"StartTime": 1500,
- "Position": 416
+ "Position": 416,
+ "HyperDash": false
},
{
"StartTime": 1562,
- "Position": 377.159973
+ "Position": 377.159973,
+ "HyperDash": false
},
{
"StartTime": 1625,
- "Position": 367
+ "Position": 367,
+ "HyperDash": false
},
{
"StartTime": 1687,
- "Position": 374.159973
+ "Position": 374.159973,
+ "HyperDash": false
},
{
"StartTime": 1750,
- "Position": 353
+ "Position": 353,
+ "HyperDash": false
},
{
"StartTime": 1812,
- "Position": 329.159973
+ "Position": 329.159973,
+ "HyperDash": false
},
{
"StartTime": 1875,
- "Position": 288
+ "Position": 288,
+ "HyperDash": false
},
{
"StartTime": 1937,
- "Position": 259.159973
+ "Position": 259.159973,
+ "HyperDash": false
},
{
"StartTime": 2000,
- "Position": 256
+ "Position": 256,
+ "HyperDash": false
},
{
"StartTime": 2058,
- "Position": 232.44
+ "Position": 232.44,
+ "HyperDash": false
},
{
"StartTime": 2116,
- "Position": 222.879974
+ "Position": 222.879974,
+ "HyperDash": false
},
{
"StartTime": 2174,
- "Position": 185.319992
+ "Position": 185.319992,
+ "HyperDash": false
},
{
"StartTime": 2232,
- "Position": 177.76001
+ "Position": 177.76001,
+ "HyperDash": false
},
{
"StartTime": 2290,
- "Position": 162.200012
+ "Position": 162.200012,
+ "HyperDash": false
},
{
"StartTime": 2348,
- "Position": 158.639984
+ "Position": 158.639984,
+ "HyperDash": false
},
{
"StartTime": 2406,
- "Position": 111.079994
+ "Position": 111.079994,
+ "HyperDash": false
},
{
"StartTime": 2500,
- "Position": 96
+ "Position": 96,
+ "HyperDash": false
}
]
},
@@ -139,71 +172,88 @@
"StartTime": 3000,
"Objects": [{
"StartTime": 3000,
- "Position": 18
+ "Position": 18,
+ "HyperDash": false
},
{
"StartTime": 3062,
- "Position": 249
+ "Position": 249,
+ "HyperDash": false
},
{
"StartTime": 3125,
- "Position": 184
+ "Position": 184,
+ "HyperDash": false
},
{
"StartTime": 3187,
- "Position": 477
+ "Position": 477,
+ "HyperDash": false
},
{
"StartTime": 3250,
- "Position": 43
+ "Position": 43,
+ "HyperDash": false
},
{
"StartTime": 3312,
- "Position": 494
+ "Position": 494,
+ "HyperDash": false
},
{
"StartTime": 3375,
- "Position": 135
+ "Position": 135,
+ "HyperDash": false
},
{
"StartTime": 3437,
- "Position": 30
+ "Position": 30,
+ "HyperDash": false
},
{
"StartTime": 3500,
- "Position": 11
+ "Position": 11,
+ "HyperDash": false
},
{
"StartTime": 3562,
- "Position": 239
+ "Position": 239,
+ "HyperDash": false
},
{
"StartTime": 3625,
- "Position": 505
+ "Position": 505,
+ "HyperDash": false
},
{
"StartTime": 3687,
- "Position": 353
+ "Position": 353,
+ "HyperDash": false
},
{
"StartTime": 3750,
- "Position": 136
+ "Position": 136,
+ "HyperDash": false
},
{
"StartTime": 3812,
- "Position": 135
+ "Position": 135,
+ "HyperDash": false
},
{
"StartTime": 3875,
- "Position": 346
+ "Position": 346,
+ "HyperDash": false
},
{
"StartTime": 3937,
- "Position": 39
+ "Position": 39,
+ "HyperDash": false
},
{
"StartTime": 4000,
- "Position": 300
+ "Position": 300,
+ "HyperDash": false
}
]
},
@@ -211,71 +261,88 @@
"StartTime": 4500,
"Objects": [{
"StartTime": 4500,
- "Position": 398
+ "Position": 398,
+ "HyperDash": false
},
{
"StartTime": 4562,
- "Position": 151
+ "Position": 151,
+ "HyperDash": false
},
{
"StartTime": 4625,
- "Position": 73
+ "Position": 73,
+ "HyperDash": false
},
{
"StartTime": 4687,
- "Position": 311
+ "Position": 311,
+ "HyperDash": false
},
{
"StartTime": 4750,
- "Position": 90
+ "Position": 90,
+ "HyperDash": false
},
{
"StartTime": 4812,
- "Position": 264
+ "Position": 264,
+ "HyperDash": false
},
{
"StartTime": 4875,
- "Position": 477
+ "Position": 477,
+ "HyperDash": false
},
{
"StartTime": 4937,
- "Position": 473
+ "Position": 473,
+ "HyperDash": false
},
{
"StartTime": 5000,
- "Position": 120
+ "Position": 120,
+ "HyperDash": false
},
{
"StartTime": 5062,
- "Position": 115
+ "Position": 115,
+ "HyperDash": false
},
{
"StartTime": 5125,
- "Position": 163
+ "Position": 163,
+ "HyperDash": false
},
{
"StartTime": 5187,
- "Position": 447
+ "Position": 447,
+ "HyperDash": false
},
{
"StartTime": 5250,
- "Position": 72
+ "Position": 72,
+ "HyperDash": false
},
{
"StartTime": 5312,
- "Position": 257
+ "Position": 257,
+ "HyperDash": false
},
{
"StartTime": 5375,
- "Position": 153
+ "Position": 153,
+ "HyperDash": false
},
{
"StartTime": 5437,
- "Position": 388
+ "Position": 388,
+ "HyperDash": false
},
{
"StartTime": 5500,
- "Position": 336
+ "Position": 336,
+ "HyperDash": false
}
]
},
@@ -283,39 +350,48 @@
"StartTime": 6000,
"Objects": [{
"StartTime": 6000,
- "Position": 13
+ "Position": 13,
+ "HyperDash": false
},
{
"StartTime": 6062,
- "Position": 429
+ "Position": 429,
+ "HyperDash": false
},
{
"StartTime": 6125,
- "Position": 381
+ "Position": 381,
+ "HyperDash": false
},
{
"StartTime": 6187,
- "Position": 186
+ "Position": 186,
+ "HyperDash": false
},
{
"StartTime": 6250,
- "Position": 267
+ "Position": 267,
+ "HyperDash": false
},
{
"StartTime": 6312,
- "Position": 305
+ "Position": 305,
+ "HyperDash": false
},
{
"StartTime": 6375,
- "Position": 456
+ "Position": 456,
+ "HyperDash": false
},
{
"StartTime": 6437,
- "Position": 26
+ "Position": 26,
+ "HyperDash": false
},
{
"StartTime": 6500,
- "Position": 238
+ "Position": 238,
+ "HyperDash": false
}
]
},
@@ -323,71 +399,88 @@
"StartTime": 7000,
"Objects": [{
"StartTime": 7000,
- "Position": 256
+ "Position": 256,
+ "HyperDash": false
},
{
"StartTime": 7062,
- "Position": 262.84
+ "Position": 262.84,
+ "HyperDash": false
},
{
"StartTime": 7125,
- "Position": 295
+ "Position": 295,
+ "HyperDash": false
},
{
"StartTime": 7187,
- "Position": 303.84
+ "Position": 303.84,
+ "HyperDash": false
},
{
"StartTime": 7250,
- "Position": 336
+ "Position": 336,
+ "HyperDash": false
},
{
"StartTime": 7312,
- "Position": 319.16
+ "Position": 319.16,
+ "HyperDash": false
},
{
"StartTime": 7375,
- "Position": 306
+ "Position": 306,
+ "HyperDash": false
},
{
"StartTime": 7437,
- "Position": 272.16
+ "Position": 272.16,
+ "HyperDash": false
},
{
"StartTime": 7500,
- "Position": 256
+ "Position": 256,
+ "HyperDash": false
},
{
"StartTime": 7562,
- "Position": 255.84
+ "Position": 255.84,
+ "HyperDash": false
},
{
"StartTime": 7625,
- "Position": 300
+ "Position": 300,
+ "HyperDash": false
},
{
"StartTime": 7687,
- "Position": 320.84
+ "Position": 320.84,
+ "HyperDash": false
},
{
"StartTime": 7750,
- "Position": 336
+ "Position": 336,
+ "HyperDash": false
},
{
"StartTime": 7803,
- "Position": 319.04
+ "Position": 319.04,
+ "HyperDash": false
},
{
"StartTime": 7857,
- "Position": 283.76
+ "Position": 283.76,
+ "HyperDash": false
},
{
"StartTime": 7910,
- "Position": 265.8
+ "Position": 265.8,
+ "HyperDash": false
},
{
"StartTime": 8000,
- "Position": 256
+ "Position": 256,
+ "HyperDash": false
}
]
},
@@ -395,167 +488,208 @@
"StartTime": 8500,
"Objects": [{
"StartTime": 8500,
- "Position": 32
+ "Position": 32,
+ "HyperDash": false
},
{
"StartTime": 8562,
- "Position": 21.8515015
+ "Position": 21.8515015,
+ "HyperDash": false
},
{
"StartTime": 8625,
- "Position": 44.5659637
+ "Position": 44.5659637,
+ "HyperDash": false
},
{
"StartTime": 8687,
- "Position": 33.3433228
+ "Position": 33.3433228,
+ "HyperDash": false
},
{
"StartTime": 8750,
- "Position": 63.58974
+ "Position": 63.58974,
+ "HyperDash": false
},
{
"StartTime": 8812,
- "Position": 71.23422
+ "Position": 71.23422,
+ "HyperDash": false
},
{
"StartTime": 8875,
- "Position": 62.7117844
+ "Position": 62.7117844,
+ "HyperDash": false
},
{
"StartTime": 8937,
- "Position": 65.52607
+ "Position": 65.52607,
+ "HyperDash": false
},
{
"StartTime": 9000,
- "Position": 101.81015
+ "Position": 101.81015,
+ "HyperDash": false
},
{
"StartTime": 9062,
- "Position": 134.47818
+ "Position": 134.47818,
+ "HyperDash": false
},
{
"StartTime": 9125,
- "Position": 141.414444
+ "Position": 141.414444,
+ "HyperDash": false
},
{
"StartTime": 9187,
- "Position": 164.1861
+ "Position": 164.1861,
+ "HyperDash": false
},
{
"StartTime": 9250,
- "Position": 176.600418
+ "Position": 176.600418,
+ "HyperDash": false
},
{
"StartTime": 9312,
- "Position": 184.293015
+ "Position": 184.293015,
+ "HyperDash": false
},
{
"StartTime": 9375,
- "Position": 212.2076
+ "Position": 212.2076,
+ "HyperDash": false
},
{
"StartTime": 9437,
- "Position": 236.438324
+ "Position": 236.438324,
+ "HyperDash": false
},
{
"StartTime": 9500,
- "Position": 237.2304
+ "Position": 237.2304,
+ "HyperDash": false
},
{
"StartTime": 9562,
- "Position": 241.253983
+ "Position": 241.253983,
+ "HyperDash": false
},
{
"StartTime": 9625,
- "Position": 233.950623
+ "Position": 233.950623,
+ "HyperDash": false
},
{
"StartTime": 9687,
- "Position": 265.3786
+ "Position": 265.3786,
+ "HyperDash": false
},
{
"StartTime": 9750,
- "Position": 236.8865
+ "Position": 236.8865,
+ "HyperDash": false
},
{
"StartTime": 9812,
- "Position": 273.38974
+ "Position": 273.38974,
+ "HyperDash": false
},
{
"StartTime": 9875,
- "Position": 267.701874
+ "Position": 267.701874,
+ "HyperDash": false
},
{
"StartTime": 9937,
- "Position": 263.2331
+ "Position": 263.2331,
+ "HyperDash": false
},
{
"StartTime": 10000,
- "Position": 270.339874
+ "Position": 270.339874,
+ "HyperDash": false
},
{
"StartTime": 10062,
- "Position": 291.9349
+ "Position": 291.9349,
+ "HyperDash": false
},
{
"StartTime": 10125,
- "Position": 294.2969
+ "Position": 294.2969,
+ "HyperDash": false
},
{
"StartTime": 10187,
- "Position": 307.834137
+ "Position": 307.834137,
+ "HyperDash": false
},
{
"StartTime": 10250,
- "Position": 310.6449
+ "Position": 310.6449,
+ "HyperDash": false
},
{
"StartTime": 10312,
- "Position": 344.746338
+ "Position": 344.746338,
+ "HyperDash": false
},
{
"StartTime": 10375,
- "Position": 349.21875
+ "Position": 349.21875,
+ "HyperDash": false
},
{
"StartTime": 10437,
- "Position": 373.943
+ "Position": 373.943,
+ "HyperDash": false
},
{
"StartTime": 10500,
- "Position": 401.0588
+ "Position": 401.0588,
+ "HyperDash": false
},
{
"StartTime": 10558,
- "Position": 421.21347
+ "Position": 421.21347,
+ "HyperDash": false
},
{
"StartTime": 10616,
- "Position": 431.6034
+ "Position": 431.6034,
+ "HyperDash": false
},
{
"StartTime": 10674,
- "Position": 433.835754
+ "Position": 433.835754,
+ "HyperDash": false
},
{
"StartTime": 10732,
- "Position": 452.5042
+ "Position": 452.5042,
+ "HyperDash": false
},
{
"StartTime": 10790,
- "Position": 486.290955
+ "Position": 486.290955,
+ "HyperDash": false
},
{
"StartTime": 10848,
- "Position": 488.943237
+ "Position": 488.943237,
+ "HyperDash": false
},
{
"StartTime": 10906,
- "Position": 493.3372
+ "Position": 493.3372,
+ "HyperDash": false
},
{
"StartTime": 10999,
- "Position": 508.166229
+ "Position": 508.166229,
+ "HyperDash": false
}
]
},
@@ -563,39 +697,48 @@
"StartTime": 11500,
"Objects": [{
"StartTime": 11500,
- "Position": 97
+ "Position": 97,
+ "HyperDash": false
},
{
"StartTime": 11562,
- "Position": 267
+ "Position": 267,
+ "HyperDash": false
},
{
"StartTime": 11625,
- "Position": 116
+ "Position": 116,
+ "HyperDash": false
},
{
"StartTime": 11687,
- "Position": 451
+ "Position": 451,
+ "HyperDash": false
},
{
"StartTime": 11750,
- "Position": 414
+ "Position": 414,
+ "HyperDash": false
},
{
"StartTime": 11812,
- "Position": 88
+ "Position": 88,
+ "HyperDash": false
},
{
"StartTime": 11875,
- "Position": 257
+ "Position": 257,
+ "HyperDash": false
},
{
"StartTime": 11937,
- "Position": 175
+ "Position": 175,
+ "HyperDash": false
},
{
"StartTime": 12000,
- "Position": 38
+ "Position": 38,
+ "HyperDash": false
}
]
},
@@ -603,263 +746,328 @@
"StartTime": 12500,
"Objects": [{
"StartTime": 12500,
- "Position": 512
+ "Position": 512,
+ "HyperDash": false
},
{
"StartTime": 12562,
- "Position": 494.3132
+ "Position": 494.3132,
+ "HyperDash": false
},
{
"StartTime": 12625,
- "Position": 461.3089
+ "Position": 461.3089,
+ "HyperDash": false
},
{
"StartTime": 12687,
- "Position": 469.6221
+ "Position": 469.6221,
+ "HyperDash": false
},
{
"StartTime": 12750,
- "Position": 441.617767
+ "Position": 441.617767,
+ "HyperDash": false
},
{
"StartTime": 12812,
- "Position": 402.930969
+ "Position": 402.930969,
+ "HyperDash": false
},
{
"StartTime": 12875,
- "Position": 407.926666
+ "Position": 407.926666,
+ "HyperDash": false
},
{
"StartTime": 12937,
- "Position": 364.239868
+ "Position": 364.239868,
+ "HyperDash": false
},
{
"StartTime": 13000,
- "Position": 353.235535
+ "Position": 353.235535,
+ "HyperDash": false
},
{
"StartTime": 13062,
- "Position": 320.548767
+ "Position": 320.548767,
+ "HyperDash": false
},
{
"StartTime": 13125,
- "Position": 303.544434
+ "Position": 303.544434,
+ "HyperDash": false
},
{
"StartTime": 13187,
- "Position": 295.857635
+ "Position": 295.857635,
+ "HyperDash": false
},
{
"StartTime": 13250,
- "Position": 265.853333
+ "Position": 265.853333,
+ "HyperDash": false
},
{
"StartTime": 13312,
- "Position": 272.166534
+ "Position": 272.166534,
+ "HyperDash": false
},
{
"StartTime": 13375,
- "Position": 240.1622
+ "Position": 240.1622,
+ "HyperDash": false
},
{
"StartTime": 13437,
- "Position": 229.4754
+ "Position": 229.4754,
+ "HyperDash": false
},
{
"StartTime": 13500,
- "Position": 194.471069
+ "Position": 194.471069,
+ "HyperDash": false
},
{
"StartTime": 13562,
- "Position": 158.784271
+ "Position": 158.784271,
+ "HyperDash": false
},
{
"StartTime": 13625,
- "Position": 137.779968
+ "Position": 137.779968,
+ "HyperDash": false
},
{
"StartTime": 13687,
- "Position": 147.09314
+ "Position": 147.09314,
+ "HyperDash": false
},
{
"StartTime": 13750,
- "Position": 122.088837
+ "Position": 122.088837,
+ "HyperDash": false
},
{
"StartTime": 13812,
- "Position": 77.40204
+ "Position": 77.40204,
+ "HyperDash": false
},
{
"StartTime": 13875,
- "Position": 79.3977356
+ "Position": 79.3977356,
+ "HyperDash": false
},
{
"StartTime": 13937,
- "Position": 56.710907
+ "Position": 56.710907,
+ "HyperDash": false
},
{
"StartTime": 14000,
- "Position": 35.7066345
+ "Position": 35.7066345,
+ "HyperDash": false
},
{
"StartTime": 14062,
- "Position": 1.01980591
+ "Position": 1.01980591,
+ "HyperDash": false
},
{
"StartTime": 14125,
- "Position": 0
+ "Position": 0,
+ "HyperDash": false
},
{
"StartTime": 14187,
- "Position": 21.7696266
+ "Position": 21.7696266,
+ "HyperDash": false
},
{
"StartTime": 14250,
- "Position": 49.0119171
+ "Position": 49.0119171,
+ "HyperDash": false
},
{
"StartTime": 14312,
- "Position": 48.9488258
+ "Position": 48.9488258,
+ "HyperDash": false
},
{
"StartTime": 14375,
- "Position": 87.19112
+ "Position": 87.19112,
+ "HyperDash": false
},
{
"StartTime": 14437,
- "Position": 97.12803
+ "Position": 97.12803,
+ "HyperDash": false
},
{
"StartTime": 14500,
- "Position": 118.370323
+ "Position": 118.370323,
+ "HyperDash": false
},
{
"StartTime": 14562,
- "Position": 130.307236
+ "Position": 130.307236,
+ "HyperDash": false
},
{
"StartTime": 14625,
- "Position": 154.549515
+ "Position": 154.549515,
+ "HyperDash": false
},
{
"StartTime": 14687,
- "Position": 190.486435
+ "Position": 190.486435,
+ "HyperDash": false
},
{
"StartTime": 14750,
- "Position": 211.728714
+ "Position": 211.728714,
+ "HyperDash": false
},
{
"StartTime": 14812,
- "Position": 197.665634
+ "Position": 197.665634,
+ "HyperDash": false
},
{
"StartTime": 14875,
- "Position": 214.907928
+ "Position": 214.907928,
+ "HyperDash": false
},
{
"StartTime": 14937,
- "Position": 263.844849
+ "Position": 263.844849,
+ "HyperDash": false
},
{
"StartTime": 15000,
- "Position": 271.087128
+ "Position": 271.087128,
+ "HyperDash": false
},
{
"StartTime": 15062,
- "Position": 270.024017
+ "Position": 270.024017,
+ "HyperDash": false
},
{
"StartTime": 15125,
- "Position": 308.266327
+ "Position": 308.266327,
+ "HyperDash": false
},
{
"StartTime": 15187,
- "Position": 313.203247
+ "Position": 313.203247,
+ "HyperDash": false
},
{
"StartTime": 15250,
- "Position": 328.445526
+ "Position": 328.445526,
+ "HyperDash": false
},
{
"StartTime": 15312,
- "Position": 370.382446
+ "Position": 370.382446,
+ "HyperDash": false
},
{
"StartTime": 15375,
- "Position": 387.624725
+ "Position": 387.624725,
+ "HyperDash": false
},
{
"StartTime": 15437,
- "Position": 421.561646
+ "Position": 421.561646,
+ "HyperDash": false
},
{
"StartTime": 15500,
- "Position": 423.803925
+ "Position": 423.803925,
+ "HyperDash": false
},
{
"StartTime": 15562,
- "Position": 444.740845
+ "Position": 444.740845,
+ "HyperDash": false
},
{
"StartTime": 15625,
- "Position": 469.983124
+ "Position": 469.983124,
+ "HyperDash": false
},
{
"StartTime": 15687,
- "Position": 473.920044
+ "Position": 473.920044,
+ "HyperDash": false
},
{
"StartTime": 15750,
- "Position": 501.162323
+ "Position": 501.162323,
+ "HyperDash": false
},
{
"StartTime": 15812,
- "Position": 488.784332
+ "Position": 488.784332,
+ "HyperDash": false
},
{
"StartTime": 15875,
- "Position": 466.226227
+ "Position": 466.226227,
+ "HyperDash": false
},
{
"StartTime": 15937,
- "Position": 445.978638
+ "Position": 445.978638,
+ "HyperDash": false
},
{
"StartTime": 16000,
- "Position": 446.420532
+ "Position": 446.420532,
+ "HyperDash": false
},
{
"StartTime": 16058,
- "Position": 428.4146
+ "Position": 428.4146,
+ "HyperDash": false
},
{
"StartTime": 16116,
- "Position": 420.408844
+ "Position": 420.408844,
+ "HyperDash": false
},
{
"StartTime": 16174,
- "Position": 374.402924
+ "Position": 374.402924,
+ "HyperDash": false
},
{
"StartTime": 16232,
- "Position": 371.397156
+ "Position": 371.397156,
+ "HyperDash": false
},
{
"StartTime": 16290,
- "Position": 350.391235
+ "Position": 350.391235,
+ "HyperDash": false
},
{
"StartTime": 16348,
- "Position": 340.385468
+ "Position": 340.385468,
+ "HyperDash": false
},
{
"StartTime": 16406,
- "Position": 337.3797
+ "Position": 337.3797,
+ "HyperDash": false
},
{
"StartTime": 16500,
- "Position": 291.1977
+ "Position": 291.1977,
+ "HyperDash": false
}
]
},
@@ -867,71 +1075,88 @@
"StartTime": 17000,
"Objects": [{
"StartTime": 17000,
- "Position": 256
+ "Position": 256,
+ "HyperDash": false
},
{
"StartTime": 17062,
- "Position": 247.16
+ "Position": 247.16,
+ "HyperDash": false
},
{
"StartTime": 17125,
- "Position": 211
+ "Position": 211,
+ "HyperDash": false
},
{
"StartTime": 17187,
- "Position": 183.16
+ "Position": 183.16,
+ "HyperDash": false
},
{
"StartTime": 17250,
- "Position": 176
+ "Position": 176,
+ "HyperDash": false
},
{
"StartTime": 17312,
- "Position": 204.84
+ "Position": 204.84,
+ "HyperDash": false
},
{
"StartTime": 17375,
- "Position": 218
+ "Position": 218,
+ "HyperDash": false
},
{
"StartTime": 17437,
- "Position": 231.84
+ "Position": 231.84,
+ "HyperDash": false
},
{
"StartTime": 17500,
- "Position": 256
+ "Position": 256,
+ "HyperDash": false
},
{
"StartTime": 17562,
- "Position": 229.16
+ "Position": 229.16,
+ "HyperDash": false
},
{
"StartTime": 17625,
- "Position": 227
+ "Position": 227,
+ "HyperDash": false
},
{
"StartTime": 17687,
- "Position": 186.16
+ "Position": 186.16,
+ "HyperDash": false
},
{
"StartTime": 17750,
- "Position": 176
+ "Position": 176,
+ "HyperDash": false
},
{
"StartTime": 17803,
- "Position": 211.959991
+ "Position": 211.959991,
+ "HyperDash": false
},
{
"StartTime": 17857,
- "Position": 197.23999
+ "Position": 197.23999,
+ "HyperDash": false
},
{
"StartTime": 17910,
- "Position": 225.200012
+ "Position": 225.200012,
+ "HyperDash": false
},
{
"StartTime": 18000,
- "Position": 256
+ "Position": 256,
+ "HyperDash": false
}
]
},
@@ -939,71 +1164,88 @@
"StartTime": 18500,
"Objects": [{
"StartTime": 18500,
- "Position": 437
+ "Position": 437,
+ "HyperDash": false
},
{
"StartTime": 18559,
- "Position": 289
+ "Position": 289,
+ "HyperDash": false
},
{
"StartTime": 18618,
- "Position": 464
+ "Position": 464,
+ "HyperDash": false
},
{
"StartTime": 18678,
- "Position": 36
+ "Position": 36,
+ "HyperDash": false
},
{
"StartTime": 18737,
- "Position": 378
+ "Position": 378,
+ "HyperDash": false
},
{
"StartTime": 18796,
- "Position": 297
+ "Position": 297,
+ "HyperDash": false
},
{
"StartTime": 18856,
- "Position": 418
+ "Position": 418,
+ "HyperDash": false
},
{
"StartTime": 18915,
- "Position": 329
+ "Position": 329,
+ "HyperDash": false
},
{
"StartTime": 18975,
- "Position": 338
+ "Position": 338,
+ "HyperDash": false
},
{
"StartTime": 19034,
- "Position": 394
+ "Position": 394,
+ "HyperDash": false
},
{
"StartTime": 19093,
- "Position": 40
+ "Position": 40,
+ "HyperDash": false
},
{
"StartTime": 19153,
- "Position": 13
+ "Position": 13,
+ "HyperDash": false
},
{
"StartTime": 19212,
- "Position": 80
+ "Position": 80,
+ "HyperDash": false
},
{
"StartTime": 19271,
- "Position": 138
+ "Position": 138,
+ "HyperDash": false
},
{
"StartTime": 19331,
- "Position": 311
+ "Position": 311,
+ "HyperDash": false
},
{
"StartTime": 19390,
- "Position": 216
+ "Position": 216,
+ "HyperDash": false
},
{
"StartTime": 19450,
- "Position": 310
+ "Position": 310,
+ "HyperDash": false
}
]
},
@@ -1011,263 +1253,328 @@
"StartTime": 19875,
"Objects": [{
"StartTime": 19875,
- "Position": 216
+ "Position": 216,
+ "HyperDash": false
},
{
"StartTime": 19937,
- "Position": 228.307053
+ "Position": 228.307053,
+ "HyperDash": false
},
{
"StartTime": 20000,
- "Position": 214.036865
+ "Position": 214.036865,
+ "HyperDash": false
},
{
"StartTime": 20062,
- "Position": 224.312088
+ "Position": 224.312088,
+ "HyperDash": false
},
{
"StartTime": 20125,
- "Position": 253.838928
+ "Position": 253.838928,
+ "HyperDash": false
},
{
"StartTime": 20187,
- "Position": 259.9743
+ "Position": 259.9743,
+ "HyperDash": false
},
{
"StartTime": 20250,
- "Position": 299.999146
+ "Position": 299.999146,
+ "HyperDash": false
},
{
"StartTime": 20312,
- "Position": 289.669067
+ "Position": 289.669067,
+ "HyperDash": false
},
{
"StartTime": 20375,
- "Position": 317.446747
+ "Position": 317.446747,
+ "HyperDash": false
},
{
"StartTime": 20437,
- "Position": 344.750275
+ "Position": 344.750275,
+ "HyperDash": false
},
{
"StartTime": 20500,
- "Position": 328.0156
+ "Position": 328.0156,
+ "HyperDash": false
},
{
"StartTime": 20562,
- "Position": 331.472168
+ "Position": 331.472168,
+ "HyperDash": false
},
{
"StartTime": 20625,
- "Position": 302.165466
+ "Position": 302.165466,
+ "HyperDash": false
},
{
"StartTime": 20687,
- "Position": 303.044617
+ "Position": 303.044617,
+ "HyperDash": false
},
{
"StartTime": 20750,
- "Position": 306.457367
+ "Position": 306.457367,
+ "HyperDash": false
},
{
"StartTime": 20812,
- "Position": 265.220581
+ "Position": 265.220581,
+ "HyperDash": false
},
{
"StartTime": 20875,
- "Position": 270.3294
+ "Position": 270.3294,
+ "HyperDash": false
},
{
"StartTime": 20937,
- "Position": 257.57605
+ "Position": 257.57605,
+ "HyperDash": false
},
{
"StartTime": 21000,
- "Position": 247.803329
+ "Position": 247.803329,
+ "HyperDash": false
},
{
"StartTime": 21062,
- "Position": 225.958359
+ "Position": 225.958359,
+ "HyperDash": false
},
{
"StartTime": 21125,
- "Position": 201.79332
+ "Position": 201.79332,
+ "HyperDash": false
},
{
"StartTime": 21187,
- "Position": 170.948349
+ "Position": 170.948349,
+ "HyperDash": false
},
{
"StartTime": 21250,
- "Position": 146.78334
+ "Position": 146.78334,
+ "HyperDash": false
},
{
"StartTime": 21312,
- "Position": 149.93837
+ "Position": 149.93837,
+ "HyperDash": false
},
{
"StartTime": 21375,
- "Position": 119.121056
+ "Position": 119.121056,
+ "HyperDash": false
},
{
"StartTime": 21437,
- "Position": 133.387573
+ "Position": 133.387573,
+ "HyperDash": false
},
{
"StartTime": 21500,
- "Position": 117.503014
+ "Position": 117.503014,
+ "HyperDash": false
},
{
"StartTime": 21562,
- "Position": 103.749374
+ "Position": 103.749374,
+ "HyperDash": false
},
{
"StartTime": 21625,
- "Position": 127.165535
+ "Position": 127.165535,
+ "HyperDash": false
},
{
"StartTime": 21687,
- "Position": 113.029991
+ "Position": 113.029991,
+ "HyperDash": false
},
{
"StartTime": 21750,
- "Position": 101.547928
+ "Position": 101.547928,
+ "HyperDash": false
},
{
"StartTime": 21812,
- "Position": 133.856232
+ "Position": 133.856232,
+ "HyperDash": false
},
{
"StartTime": 21875,
- "Position": 124.28746
+ "Position": 124.28746,
+ "HyperDash": false
},
{
"StartTime": 21937,
- "Position": 121.754929
+ "Position": 121.754929,
+ "HyperDash": false
},
{
"StartTime": 22000,
- "Position": 155.528732
+ "Position": 155.528732,
+ "HyperDash": false
},
{
"StartTime": 22062,
- "Position": 142.1691
+ "Position": 142.1691,
+ "HyperDash": false
},
{
"StartTime": 22125,
- "Position": 186.802155
+ "Position": 186.802155,
+ "HyperDash": false
},
{
"StartTime": 22187,
- "Position": 198.6452
+ "Position": 198.6452,
+ "HyperDash": false
},
{
"StartTime": 22250,
- "Position": 191.892181
+ "Position": 191.892181,
+ "HyperDash": false
},
{
"StartTime": 22312,
- "Position": 232.713028
+ "Position": 232.713028,
+ "HyperDash": false
},
{
"StartTime": 22375,
- "Position": 240.4715
+ "Position": 240.4715,
+ "HyperDash": false
},
{
"StartTime": 22437,
- "Position": 278.3719
+ "Position": 278.3719,
+ "HyperDash": false
},
{
"StartTime": 22500,
- "Position": 288.907257
+ "Position": 288.907257,
+ "HyperDash": false
},
{
"StartTime": 22562,
- "Position": 297.353119
+ "Position": 297.353119,
+ "HyperDash": false
},
{
"StartTime": 22625,
- "Position": 301.273376
+ "Position": 301.273376,
+ "HyperDash": false
},
{
"StartTime": 22687,
- "Position": 339.98288
+ "Position": 339.98288,
+ "HyperDash": false
},
{
"StartTime": 22750,
- "Position": 353.078552
+ "Position": 353.078552,
+ "HyperDash": false
},
{
"StartTime": 22812,
- "Position": 363.8958
+ "Position": 363.8958,
+ "HyperDash": false
},
{
"StartTime": 22875,
- "Position": 398.054047
+ "Position": 398.054047,
+ "HyperDash": false
},
{
"StartTime": 22937,
- "Position": 419.739441
+ "Position": 419.739441,
+ "HyperDash": false
},
{
"StartTime": 23000,
- "Position": 435.178467
+ "Position": 435.178467,
+ "HyperDash": false
},
{
"StartTime": 23062,
- "Position": 420.8687
+ "Position": 420.8687,
+ "HyperDash": false
},
{
"StartTime": 23125,
- "Position": 448.069977
+ "Position": 448.069977,
+ "HyperDash": false
},
{
"StartTime": 23187,
- "Position": 425.688477
+ "Position": 425.688477,
+ "HyperDash": false
},
{
"StartTime": 23250,
- "Position": 426.9612
+ "Position": 426.9612,
+ "HyperDash": false
},
{
"StartTime": 23312,
- "Position": 454.92807
+ "Position": 454.92807,
+ "HyperDash": false
},
{
"StartTime": 23375,
- "Position": 439.749878
+ "Position": 439.749878,
+ "HyperDash": false
},
{
"StartTime": 23433,
- "Position": 440.644684
+ "Position": 440.644684,
+ "HyperDash": false
},
{
"StartTime": 23491,
- "Position": 445.7359
+ "Position": 445.7359,
+ "HyperDash": false
},
{
"StartTime": 23549,
- "Position": 432.0944
+ "Position": 432.0944,
+ "HyperDash": false
},
{
"StartTime": 23607,
- "Position": 415.796173
+ "Position": 415.796173,
+ "HyperDash": false
},
{
"StartTime": 23665,
- "Position": 407.897461
+ "Position": 407.897461,
+ "HyperDash": false
},
{
"StartTime": 23723,
- "Position": 409.462555
+ "Position": 409.462555,
+ "HyperDash": false
},
{
"StartTime": 23781,
- "Position": 406.53775
+ "Position": 406.53775,
+ "HyperDash": false
},
{
"StartTime": 23874,
- "Position": 408.720825
+ "Position": 408.720825,
+ "HyperDash": false
}
]
}
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash-expected-conversion.json
new file mode 100644
index 0000000000..b2e9431f13
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash-expected-conversion.json
@@ -0,0 +1,19 @@
+{
+ "Mappings": [{
+ "StartTime": 369,
+ "Objects": [{
+ "StartTime": 369,
+ "Position": 0,
+ "HyperDash": true
+ }]
+ },
+ {
+ "StartTime": 450,
+ "Objects": [{
+ "StartTime": 450,
+ "Position": 512,
+ "HyperDash": false
+ }]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash.osu
new file mode 100644
index 0000000000..db07f8c30e
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash.osu
@@ -0,0 +1,21 @@
+osu file format v14
+
+[General]
+StackLeniency: 0.7
+Mode: 2
+
+[Difficulty]
+HPDrainRate:6
+CircleSize:4
+OverallDifficulty:9.6
+ApproachRate:9.6
+SliderMultiplier:1.9
+SliderTickRate:1
+
+[TimingPoints]
+2169,266.666666666667,4,2,1,70,1,0
+
+
+[HitObjects]
+0,192,369,1,0,0:0:0:0:
+512,192,450,1,0,0:0:0:0:
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json
index 83f9e30800..081b574c5b 100644
--- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json
@@ -3,147 +3,183 @@
"StartTime": 369,
"Objects": [{
"StartTime": 369,
- "Position": 177
+ "Position": 177,
+ "HyperDash": false
},
{
"StartTime": 450,
- "Position": 216.539276
+ "Position": 216.539276,
+ "HyperDash": false
},
{
"StartTime": 532,
- "Position": 256.5667
+ "Position": 256.5667,
+ "HyperDash": false
},
{
"StartTime": 614,
- "Position": 296.594116
+ "Position": 296.594116,
+ "HyperDash": false
},
{
"StartTime": 696,
- "Position": 336.621521
+ "Position": 336.621521,
+ "HyperDash": false
},
{
"StartTime": 778,
- "Position": 376.99762
+ "Position": 376.99762,
+ "HyperDash": false
},
{
"StartTime": 860,
- "Position": 337.318878
+ "Position": 337.318878,
+ "HyperDash": false
},
{
"StartTime": 942,
- "Position": 297.291443
+ "Position": 297.291443,
+ "HyperDash": false
},
{
"StartTime": 1024,
- "Position": 257.264038
+ "Position": 257.264038,
+ "HyperDash": false
},
{
"StartTime": 1106,
- "Position": 217.2366
+ "Position": 217.2366,
+ "HyperDash": false
},
{
"StartTime": 1188,
- "Position": 177
+ "Position": 177,
+ "HyperDash": false
},
{
"StartTime": 1270,
- "Position": 216.818192
+ "Position": 216.818192,
+ "HyperDash": false
},
{
"StartTime": 1352,
- "Position": 256.8456
+ "Position": 256.8456,
+ "HyperDash": false
},
{
"StartTime": 1434,
- "Position": 296.873047
+ "Position": 296.873047,
+ "HyperDash": false
},
{
"StartTime": 1516,
- "Position": 336.900452
+ "Position": 336.900452,
+ "HyperDash": false
},
{
"StartTime": 1598,
- "Position": 376.99762
+ "Position": 376.99762,
+ "HyperDash": false
},
{
"StartTime": 1680,
- "Position": 337.039948
+ "Position": 337.039948,
+ "HyperDash": false
},
{
"StartTime": 1762,
- "Position": 297.0125
+ "Position": 297.0125,
+ "HyperDash": false
},
{
"StartTime": 1844,
- "Position": 256.9851
+ "Position": 256.9851,
+ "HyperDash": false
},
{
"StartTime": 1926,
- "Position": 216.957672
+ "Position": 216.957672,
+ "HyperDash": false
},
{
"StartTime": 2008,
- "Position": 177
+ "Position": 177,
+ "HyperDash": false
},
{
"StartTime": 2090,
- "Position": 217.097137
+ "Position": 217.097137,
+ "HyperDash": false
},
{
"StartTime": 2172,
- "Position": 257.124573
+ "Position": 257.124573,
+ "HyperDash": false
},
{
"StartTime": 2254,
- "Position": 297.152
+ "Position": 297.152,
+ "HyperDash": false
},
{
"StartTime": 2336,
- "Position": 337.179443
+ "Position": 337.179443,
+ "HyperDash": false
},
{
"StartTime": 2418,
- "Position": 376.99762
+ "Position": 376.99762,
+ "HyperDash": false
},
{
"StartTime": 2500,
- "Position": 336.760956
+ "Position": 336.760956,
+ "HyperDash": false
},
{
"StartTime": 2582,
- "Position": 296.733643
+ "Position": 296.733643,
+ "HyperDash": false
},
{
"StartTime": 2664,
- "Position": 256.7062
+ "Position": 256.7062,
+ "HyperDash": false
},
{
"StartTime": 2746,
- "Position": 216.678772
+ "Position": 216.678772,
+ "HyperDash": false
},
{
"StartTime": 2828,
- "Position": 177
+ "Position": 177,
+ "HyperDash": false
},
{
"StartTime": 2909,
- "Position": 216.887909
+ "Position": 216.887909,
+ "HyperDash": false
},
{
"StartTime": 2991,
- "Position": 256.915344
+ "Position": 256.915344,
+ "HyperDash": false
},
{
"StartTime": 3073,
- "Position": 296.942749
+ "Position": 296.942749,
+ "HyperDash": false
},
{
"StartTime": 3155,
- "Position": 336.970184
+ "Position": 336.970184,
+ "HyperDash": false
},
{
"StartTime": 3237,
- "Position": 376.99762
+ "Position": 376.99762,
+ "HyperDash": false
}
]
}]
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json
index 7333b600fb..01f474c149 100644
--- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json
@@ -3,71 +3,88 @@
"StartTime": 369,
"Objects": [{
"StartTime": 369,
- "Position": 65
+ "Position": 65,
+ "HyperDash": false
},
{
"StartTime": 450,
- "Position": 482
+ "Position": 482,
+ "HyperDash": false
},
{
"StartTime": 532,
- "Position": 164
+ "Position": 164,
+ "HyperDash": false
},
{
"StartTime": 614,
- "Position": 315
+ "Position": 315,
+ "HyperDash": false
},
{
"StartTime": 696,
- "Position": 145
+ "Position": 145,
+ "HyperDash": false
},
{
"StartTime": 778,
- "Position": 159
+ "Position": 159,
+ "HyperDash": false
},
{
"StartTime": 860,
- "Position": 310
+ "Position": 310,
+ "HyperDash": false
},
{
"StartTime": 942,
- "Position": 441
+ "Position": 441,
+ "HyperDash": false
},
{
"StartTime": 1024,
- "Position": 428
+ "Position": 428,
+ "HyperDash": false
},
{
"StartTime": 1106,
- "Position": 243
+ "Position": 243,
+ "HyperDash": false
},
{
"StartTime": 1188,
- "Position": 422
+ "Position": 422,
+ "HyperDash": false
},
{
"StartTime": 1270,
- "Position": 481
+ "Position": 481,
+ "HyperDash": false
},
{
"StartTime": 1352,
- "Position": 104
+ "Position": 104,
+ "HyperDash": false
},
{
"StartTime": 1434,
- "Position": 473
+ "Position": 473,
+ "HyperDash": false
},
{
"StartTime": 1516,
- "Position": 135
+ "Position": 135,
+ "HyperDash": false
},
{
"StartTime": 1598,
- "Position": 360
+ "Position": 360,
+ "HyperDash": false
},
{
"StartTime": 1680,
- "Position": 123
+ "Position": 123,
+ "HyperDash": false
}
]
}]
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json
index bbc16ab912..8eaaf3bb90 100644
--- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json
@@ -3,231 +3,264 @@
"StartTime": 369,
"Objects": [{
"StartTime": 369,
- "Position": 258
+ "Position": 258,
+ "HyperDash": false
}]
},
{
"StartTime": 450,
"Objects": [{
"StartTime": 450,
- "Position": 254
+ "Position": 254,
+ "HyperDash": false
}]
},
{
"StartTime": 532,
"Objects": [{
"StartTime": 532,
- "Position": 241
+ "Position": 241,
+ "HyperDash": false
}]
},
{
"StartTime": 614,
"Objects": [{
"StartTime": 614,
- "Position": 238
+ "Position": 238,
+ "HyperDash": false
}]
},
{
"StartTime": 696,
"Objects": [{
"StartTime": 696,
- "Position": 238
+ "Position": 238,
+ "HyperDash": false
}]
},
{
"StartTime": 778,
"Objects": [{
"StartTime": 778,
- "Position": 278
+ "Position": 278,
+ "HyperDash": false
}]
},
{
"StartTime": 860,
"Objects": [{
"StartTime": 860,
- "Position": 238
+ "Position": 238,
+ "HyperDash": false
}]
},
{
"StartTime": 942,
"Objects": [{
"StartTime": 942,
- "Position": 278
+ "Position": 278,
+ "HyperDash": false
}]
},
{
"StartTime": 1024,
"Objects": [{
"StartTime": 1024,
- "Position": 238
+ "Position": 238,
+ "HyperDash": false
}]
},
{
"StartTime": 1106,
"Objects": [{
"StartTime": 1106,
- "Position": 278
+ "Position": 278,
+ "HyperDash": false
}]
},
{
"StartTime": 1188,
"Objects": [{
"StartTime": 1188,
- "Position": 278
+ "Position": 278,
+ "HyperDash": false
}]
},
{
"StartTime": 1270,
"Objects": [{
"StartTime": 1270,
- "Position": 278
+ "Position": 278,
+ "HyperDash": false
}]
},
{
"StartTime": 1352,
"Objects": [{
"StartTime": 1352,
- "Position": 238
+ "Position": 238,
+ "HyperDash": false
}]
},
{
"StartTime": 1434,
"Objects": [{
"StartTime": 1434,
- "Position": 258
+ "Position": 258,
+ "HyperDash": false
}]
},
{
"StartTime": 1516,
"Objects": [{
"StartTime": 1516,
- "Position": 253
+ "Position": 253,
+ "HyperDash": false
}]
},
{
"StartTime": 1598,
"Objects": [{
"StartTime": 1598,
- "Position": 238
+ "Position": 238,
+ "HyperDash": false
}]
},
{
"StartTime": 1680,
"Objects": [{
"StartTime": 1680,
- "Position": 260
+ "Position": 260,
+ "HyperDash": false
}]
},
{
"StartTime": 1762,
"Objects": [{
"StartTime": 1762,
- "Position": 238
+ "Position": 238,
+ "HyperDash": false
}]
},
{
"StartTime": 1844,
"Objects": [{
"StartTime": 1844,
- "Position": 278
+ "Position": 278,
+ "HyperDash": false
}]
},
{
"StartTime": 1926,
"Objects": [{
"StartTime": 1926,
- "Position": 278
+ "Position": 278,
+ "HyperDash": false
}]
},
{
"StartTime": 2008,
"Objects": [{
"StartTime": 2008,
- "Position": 238
+ "Position": 238,
+ "HyperDash": false
}]
},
{
"StartTime": 2090,
"Objects": [{
"StartTime": 2090,
- "Position": 238
+ "Position": 238,
+ "HyperDash": false
}]
},
{
"StartTime": 2172,
"Objects": [{
"StartTime": 2172,
- "Position": 243
+ "Position": 243,
+ "HyperDash": false
}]
},
{
"StartTime": 2254,
"Objects": [{
"StartTime": 2254,
- "Position": 278
+ "Position": 278,
+ "HyperDash": false
}]
},
{
"StartTime": 2336,
"Objects": [{
"StartTime": 2336,
- "Position": 278
+ "Position": 278,
+ "HyperDash": false
}]
},
{
"StartTime": 2418,
"Objects": [{
"StartTime": 2418,
- "Position": 238
+ "Position": 238,
+ "HyperDash": false
}]
},
{
"StartTime": 2500,
"Objects": [{
"StartTime": 2500,
- "Position": 258
+ "Position": 258,
+ "HyperDash": false
}]
},
{
"StartTime": 2582,
"Objects": [{
"StartTime": 2582,
- "Position": 256
+ "Position": 256,
+ "HyperDash": false
}]
},
{
"StartTime": 2664,
"Objects": [{
"StartTime": 2664,
- "Position": 242
+ "Position": 242,
+ "HyperDash": false
}]
},
{
"StartTime": 2746,
"Objects": [{
"StartTime": 2746,
- "Position": 238
+ "Position": 238,
+ "HyperDash": false
}]
},
{
"StartTime": 2828,
"Objects": [{
"StartTime": 2828,
- "Position": 238
+ "Position": 238,
+ "HyperDash": false
}]
},
{
"StartTime": 2909,
"Objects": [{
"StartTime": 2909,
- "Position": 271
+ "Position": 271,
+ "HyperDash": false
}]
},
{
"StartTime": 2991,
"Objects": [{
"StartTime": 2991,
- "Position": 254
+ "Position": 254,
+ "HyperDash": false
}]
}
]
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json
index 3bde97070c..5060389ad8 100644
--- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json
@@ -3,14 +3,16 @@
"StartTime": 3368,
"Objects": [{
"StartTime": 3368,
- "Position": 374
+ "Position": 374,
+ "HyperDash": false
}]
},
{
"StartTime": 3501,
"Objects": [{
"StartTime": 3501,
- "Position": 446
+ "Position": 446,
+ "HyperDash": false
}]
}
]
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/slider-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/slider-expected-conversion.json
index 58c52b6867..2378ba5511 100644
--- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/slider-expected-conversion.json
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/slider-expected-conversion.json
@@ -1 +1,71 @@
-{"Mappings":[{"StartTime":19184.0,"Objects":[{"StartTime":19184.0,"Position":320.0},{"StartTime":19263.0,"Position":311.730255},{"StartTime":19343.0,"Position":324.6205},{"StartTime":19423.0,"Position":343.0907},{"StartTime":19503.0,"Position":372.2917},{"StartTime":19582.0,"Position":385.194733},{"StartTime":19662.0,"Position":379.0426},{"StartTime":19742.0,"Position":385.1066},{"StartTime":19822.0,"Position":391.624664},{"StartTime":19919.0,"Position":386.27832},{"StartTime":20016.0,"Position":380.117035},{"StartTime":20113.0,"Position":381.664154},{"StartTime":20247.0,"Position":370.872864}]}]}
\ No newline at end of file
+{
+ "Mappings": [{
+ "StartTime": 19184,
+ "Objects": [{
+ "StartTime": 19184,
+ "Position": 320,
+ "HyperDash": false
+ },
+ {
+ "StartTime": 19263,
+ "Position": 311.730255,
+ "HyperDash": false
+ },
+ {
+ "StartTime": 19343,
+ "Position": 324.6205,
+ "HyperDash": false
+ },
+ {
+ "StartTime": 19423,
+ "Position": 343.0907,
+ "HyperDash": false
+ },
+ {
+ "StartTime": 19503,
+ "Position": 372.2917,
+ "HyperDash": false
+ },
+ {
+ "StartTime": 19582,
+ "Position": 385.194733,
+ "HyperDash": false
+ },
+ {
+ "StartTime": 19662,
+ "Position": 379.0426,
+ "HyperDash": false
+ },
+ {
+ "StartTime": 19742,
+ "Position": 385.1066,
+ "HyperDash": false
+ },
+ {
+ "StartTime": 19822,
+ "Position": 391.624664,
+ "HyperDash": false
+ },
+ {
+ "StartTime": 19919,
+ "Position": 386.27832,
+ "HyperDash": false
+ },
+ {
+ "StartTime": 20016,
+ "Position": 380.117035,
+ "HyperDash": false
+ },
+ {
+ "StartTime": 20113,
+ "Position": 381.664154,
+ "HyperDash": false
+ },
+ {
+ "StartTime": 20247,
+ "Position": 370.872864,
+ "HyperDash": false
+ }
+ ]
+ }]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json
index dd81947f1c..abd5b2afd1 100644
--- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json
@@ -3,18 +3,21 @@
"StartTime": 2589,
"Objects": [{
"StartTime": 2589,
- "Position": 256
+ "Position": 256,
+ "HyperDash": false
}]
},
{
"StartTime": 2915,
"Objects": [{
"StartTime": 2915,
- "Position": 65
+ "Position": 65,
+ "HyperDash": false
},
{
"StartTime": 2916,
- "Position": 482
+ "Position": 482,
+ "HyperDash": false
}
]
},
@@ -22,11 +25,13 @@
"StartTime": 3078,
"Objects": [{
"StartTime": 3078,
- "Position": 164
+ "Position": 164,
+ "HyperDash": false
},
{
"StartTime": 3079,
- "Position": 315
+ "Position": 315,
+ "HyperDash": false
}
]
},
@@ -34,11 +39,13 @@
"StartTime": 3241,
"Objects": [{
"StartTime": 3241,
- "Position": 145
+ "Position": 145,
+ "HyperDash": false
},
{
"StartTime": 3242,
- "Position": 159
+ "Position": 159,
+ "HyperDash": false
}
]
},
@@ -46,11 +53,13 @@
"StartTime": 3404,
"Objects": [{
"StartTime": 3404,
- "Position": 310
+ "Position": 310,
+ "HyperDash": false
},
{
"StartTime": 3405,
- "Position": 441
+ "Position": 441,
+ "HyperDash": false
}
]
},
@@ -58,7 +67,8 @@
"StartTime": 5197,
"Objects": [{
"StartTime": 5197,
- "Position": 256
+ "Position": 256,
+ "HyperDash": false
}]
}
]
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-expected-conversion.json
index b69b1ae056..8a7847e065 100644
--- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-expected-conversion.json
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-expected-conversion.json
@@ -3,71 +3,88 @@
"StartTime": 18500,
"Objects": [{
"StartTime": 18500,
- "Position": 65
+ "Position": 65,
+ "HyperDash": false
},
{
"StartTime": 18559,
- "Position": 482
+ "Position": 482,
+ "HyperDash": false
},
{
"StartTime": 18618,
- "Position": 164
+ "Position": 164,
+ "HyperDash": false
},
{
"StartTime": 18678,
- "Position": 315
+ "Position": 315,
+ "HyperDash": false
},
{
"StartTime": 18737,
- "Position": 145
+ "Position": 145,
+ "HyperDash": false
},
{
"StartTime": 18796,
- "Position": 159
+ "Position": 159,
+ "HyperDash": false
},
{
"StartTime": 18856,
- "Position": 310
+ "Position": 310,
+ "HyperDash": false
},
{
"StartTime": 18915,
- "Position": 441
+ "Position": 441,
+ "HyperDash": false
},
{
"StartTime": 18975,
- "Position": 428
+ "Position": 428,
+ "HyperDash": false
},
{
"StartTime": 19034,
- "Position": 243
+ "Position": 243,
+ "HyperDash": false
},
{
"StartTime": 19093,
- "Position": 422
+ "Position": 422,
+ "HyperDash": false
},
{
"StartTime": 19153,
- "Position": 481
+ "Position": 481,
+ "HyperDash": false
},
{
"StartTime": 19212,
- "Position": 104
+ "Position": 104,
+ "HyperDash": false
},
{
"StartTime": 19271,
- "Position": 473
+ "Position": 473,
+ "HyperDash": false
},
{
"StartTime": 19331,
- "Position": 135
+ "Position": 135,
+ "HyperDash": false
},
{
"StartTime": 19390,
- "Position": 360
+ "Position": 360,
+ "HyperDash": false
},
{
"StartTime": 19450,
- "Position": 123
+ "Position": 123,
+ "HyperDash": false
}
]
}]
diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist
index 8780204d5b..09ed2dd007 100644
--- a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist
@@ -32,5 +32,7 @@
XSAppIconAssets
Assets.xcassets/AppIcon.appiconset
+ CADisableMinimumFrameDurationOnPhone
+
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
index 24d2a786a0..a30e09cd29 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
@@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Game.Database;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Screens.Edit;
@@ -55,13 +56,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestDefaultSkin()
{
- AddStep("set default skin", () => skins.CurrentSkinInfo.Value = SkinInfo.Default);
+ AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLiveUnmanaged());
}
[Test]
public void TestLegacySkin()
{
- AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info);
+ AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged());
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
index 9d1f5429a1..1aa20f4737 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
@@ -9,7 +9,6 @@ using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.UI;
@@ -46,12 +45,6 @@ namespace osu.Game.Rulesets.Mania.Edit
[Resolved]
private EditorBeatmap beatmap { get; set; }
- [Resolved]
- private IScrollingInfo scrollingInfo { get; set; }
-
- [Resolved]
- private Bindable working { get; set; }
-
[Resolved]
private OsuColour colours { get; set; }
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index dc858fb54f..9fe1eb7932 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -7,16 +7,12 @@ using osu.Framework.Allocation;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Mania.Edit
{
public class ManiaSelectionHandler : EditorSelectionHandler
{
- [Resolved]
- private IScrollingInfo scrollingInfo { get; set; }
-
[Resolved]
private HitObjectComposer composer { get; set; }
diff --git a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
index 90d3c6c4c7..9f4963b022 100644
--- a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
@@ -15,9 +15,6 @@ namespace osu.Game.Rulesets.Mania.UI
public JudgementResult Result { get; private set; }
- [Resolved]
- private Column column { get; set; }
-
private SkinnableDrawable skinnableExplosion;
public PoolableHitExplosion()
diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist
index f79215cf54..dd032ef1c1 100644
--- a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist
@@ -32,5 +32,7 @@
XSAppIconAssets
Assets.xcassets/AppIcon.appiconset
+ CADisableMinimumFrameDurationOnPhone
+
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
index e698766aac..d673b7a6ac 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("create slider", () =>
{
- var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.Info);
+ var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
Child = new SkinProvidingContainer(tintingSkin)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index d073d751d0..4df8ff0b12 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -11,49 +11,65 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
public class OsuDifficultyHitObject : DifficultyHitObject
{
- private const int normalized_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
+ private const int normalised_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
private const int min_delta_time = 25;
- private const float maximum_slider_radius = normalized_radius * 2.4f;
- private const float assumed_slider_radius = normalized_radius * 1.8f;
+ private const float maximum_slider_radius = normalised_radius * 2.4f;
+ private const float assumed_slider_radius = normalised_radius * 1.8f;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
///
- /// Normalized distance from the end position of the previous to the start position of this .
+ /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms.
///
- public double JumpDistance { get; private set; }
+ public readonly double StrainTime;
///
- /// Minimum distance from the end position of the previous to the start position of this .
+ /// Normalised distance from the "lazy" end position of the previous to the start position of this .
+ ///
+ /// The "lazy" end position is the position at which the cursor ends up if the previous hitobject is followed with as minimal movement as possible (i.e. on the edge of slider follow circles).
+ ///
///
- public double MovementDistance { get; private set; }
+ public double LazyJumpDistance { get; private set; }
///
- /// Normalized distance between the start and end position of the previous .
+ /// Normalised shortest distance to consider for a jump between the previous and this .
+ ///
+ ///
+ /// This is bounded from above by , and is smaller than the former if a more natural path is able to be taken through the previous .
+ ///
+ ///
+ /// Suppose a linear slider - circle pattern.
+ ///
+ /// Following the slider lazily (see: ) will result in underestimating the true end position of the slider as being closer towards the start position.
+ /// As a result, overestimates the jump distance because the player is able to take a more natural path by following through the slider to its end,
+ /// such that the jump is felt as only starting from the slider's true end position.
+ ///
+ /// Now consider a slider - circle pattern where the circle is stacked along the path inside the slider.
+ /// In this case, the lazy end position correctly estimates the true end position of the slider and provides the more natural movement path.
+ ///
+ public double MinimumJumpDistance { get; private set; }
+
+ ///
+ /// The time taken to travel through , with a minimum value of 25ms.
+ ///
+ public double MinimumJumpTime { get; private set; }
+
+ ///
+ /// Normalised distance between the start and end position of this .
///
public double TravelDistance { get; private set; }
+ ///
+ /// The time taken to travel through , with a minimum value of 25ms for a non-zero distance.
+ ///
+ public double TravelTime { get; private set; }
+
///
/// Angle the player has to take to hit this .
/// Calculated as the angle between the circles (current-2, current-1, current).
///
public double? Angle { get; private set; }
- ///
- /// Milliseconds elapsed since the end time of the previous , with a minimum of 25ms.
- ///
- public double MovementTime { get; private set; }
-
- ///
- /// Milliseconds elapsed since the start time of the previous to the end time of the same previous , with a minimum of 25ms.
- ///
- public double TravelTime { get; private set; }
-
- ///
- /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms.
- ///
- public readonly double StrainTime;
-
private readonly OsuHitObject lastLastObject;
private readonly OsuHitObject lastObject;
@@ -71,12 +87,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
private void setDistances(double clockRate)
{
+ if (BaseObject is Slider currentSlider)
+ {
+ computeSliderCursorPosition(currentSlider);
+ TravelDistance = currentSlider.LazyTravelDistance;
+ TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time);
+ }
+
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
if (BaseObject is Spinner || lastObject is Spinner)
return;
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
- float scalingFactor = normalized_radius / (float)BaseObject.Radius;
+ float scalingFactor = normalised_radius / (float)BaseObject.Radius;
if (BaseObject.Radius < 30)
{
@@ -85,29 +108,40 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
}
Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
- JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
+
+ LazyJumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
+ MinimumJumpTime = StrainTime;
+ MinimumJumpDistance = LazyJumpDistance;
if (lastObject is Slider lastSlider)
{
- computeSliderCursorPosition(lastSlider);
- TravelDistance = lastSlider.LazyTravelDistance;
- TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
- MovementTime = Math.Max(StrainTime - TravelTime, min_delta_time);
+ double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
+ MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, min_delta_time);
+
+ //
+ // There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
+ //
+ // 1. The anti-flow pattern, where players cut the slider short in order to move to the next hitobject.
+ //
+ // <======o==> ← slider
+ // | ← most natural jump path
+ // o ← a follow-up hitcircle
+ //
+ // In this case the most natural jump path is approximated by LazyJumpDistance.
+ //
+ // 2. The flow pattern, where players follow through the slider to its visual extent into the next hitobject.
+ //
+ // <======o==>---o
+ // ↑
+ // most natural jump path
+ //
+ // In this case the most natural jump path is better approximated by a new distance called "tailJumpDistance" - the distance between the slider's tail and the next hitobject.
+ //
+ // Thus, the player is assumed to jump the minimum of these two distances in all cases.
+ //
- // Jump distance from the slider tail to the next object, as opposed to the lazy position of JumpDistance.
float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor;
-
- // For hitobjects which continue in the direction of the slider, the player will normally follow through the slider,
- // such that they're not jumping from the lazy position but rather from very close to (or the end of) the slider.
- // In such cases, a leniency is applied by also considering the jump distance from the tail of the slider, and taking the minimum jump distance.
- // Additional distance is removed based on position of jump relative to slider follow circle radius.
- // JumpDistance is the leniency distance beyond the assumed_slider_radius. tailJumpDistance is maximum_slider_radius since the full distance of radial leniency is still possible.
- MovementDistance = Math.Max(0, Math.Min(JumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius));
- }
- else
- {
- MovementTime = StrainTime;
- MovementDistance = JumpDistance;
+ MinimumJumpDistance = Math.Max(0, Math.Min(LazyJumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius));
}
if (lastLastObject != null && !(lastLastObject is Spinner))
@@ -139,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
var currCursorPosition = slider.StackedPosition;
- double scalingFactor = normalized_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
+ double scalingFactor = normalised_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
for (int i = 1; i < slider.NestedHitObjects.Count; i++)
{
@@ -167,7 +201,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
else if (currMovementObj is SliderRepeat)
{
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
- requiredMovement = normalized_radius;
+ requiredMovement = normalised_radius;
}
if (currMovementLength > requiredMovement)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index 2a8d2ce759..a6301aed6d 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -44,24 +44,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
var osuLastLastObj = (OsuDifficultyHitObject)Previous[1];
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
- double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime;
+ double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
if (osuLastObj.BaseObject is Slider && withSliders)
{
- double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to current object
- double travelVelocity = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // calculate the slider velocity from slider head to slider end.
+ double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end.
+ double movementVelocity = osuCurrObj.MinimumJumpDistance / osuCurrObj.MinimumJumpTime; // calculate the movement velocity from slider end to current object
currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity.
}
// As above, do the same for the previous hitobject.
- double prevVelocity = osuLastObj.JumpDistance / osuLastObj.StrainTime;
+ double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
if (osuLastLastObj.BaseObject is Slider && withSliders)
{
- double movementVelocity = osuLastObj.MovementDistance / osuLastObj.MovementTime;
- double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime;
+ double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime;
+ double movementVelocity = osuLastObj.MinimumJumpDistance / osuLastObj.MinimumJumpTime;
prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity);
}
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
* Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
- * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.JumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
+ * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
}
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
@@ -107,8 +107,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
if (Math.Max(prevVelocity, currVelocity) != 0)
{
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
- prevVelocity = (osuLastObj.JumpDistance + osuLastObj.TravelDistance) / osuLastObj.StrainTime;
- currVelocity = (osuCurrObj.JumpDistance + osuCurrObj.TravelDistance) / osuCurrObj.StrainTime;
+ prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime;
+ currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime;
// Scale with ratio of difference compared to 0.5 * max dist.
double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2);
@@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
// Reward for % distance slowed down compared to previous, paying attention to not award overlap
double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity)
// do not award overlap
- * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.JumpDistance, osuLastObj.JumpDistance) / 100)), 2);
+ * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.LazyJumpDistance, osuLastObj.LazyJumpDistance) / 100)), 2);
// Choose the largest bonus, multiplied by ratio.
velocityChangeBonus = Math.Max(overlapVelocityBuff, nonOverlapVelocityBuff) * distRatio;
@@ -128,10 +128,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
}
- if (osuCurrObj.TravelTime != 0)
+ if (osuLastObj.TravelTime != 0)
{
// Reward sliders based on velocity.
- sliderBonus = osuCurrObj.TravelDistance / osuCurrObj.TravelTime;
+ sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
}
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
index cb1ccf949e..21a2fc2252 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
// We also want to nerf stacks so that only the first object of the stack is accounted for.
- double stackNerf = Math.Min(1.0, (osuLoop.JumpDistance / scalingFactor) / 25.0);
+ double stackNerf = Math.Min(1.0, (osuLoop.LazyJumpDistance / scalingFactor) / 25.0);
result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime;
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index 24881d9c47..06d1ef7346 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -55,73 +55,75 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
bool firstDeltaSwitch = false;
- for (int i = Previous.Count - 2; i > 0; i--)
+ int rhythmStart = 0;
+
+ while (rhythmStart < Previous.Count - 2 && current.StartTime - Previous[rhythmStart].StartTime < history_time_max)
+ rhythmStart++;
+
+ for (int i = rhythmStart; i > 0; i--)
{
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1];
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i];
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)Previous[i + 1];
- double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now
+ double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
- if (currHistoricalDecay != 0)
+ currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
+
+ double currDelta = currObj.StrainTime;
+ double prevDelta = prevObj.StrainTime;
+ double lastDelta = lastObj.StrainTime;
+ double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
+
+ double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
+
+ windowPenalty = Math.Min(1, windowPenalty);
+
+ double effectiveRatio = windowPenalty * currRatio;
+
+ if (firstDeltaSwitch)
{
- currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
-
- double currDelta = currObj.StrainTime;
- double prevDelta = prevObj.StrainTime;
- double lastDelta = lastObj.StrainTime;
- double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
-
- double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
-
- windowPenalty = Math.Min(1, windowPenalty);
-
- double effectiveRatio = windowPenalty * currRatio;
-
- if (firstDeltaSwitch)
+ if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
{
- if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
- {
- if (islandSize < 7)
- islandSize++; // island is still progressing, count size.
- }
- else
- {
- if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
- effectiveRatio *= 0.125;
-
- if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
- effectiveRatio *= 0.25;
-
- if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
- effectiveRatio *= 0.25;
-
- if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
- effectiveRatio *= 0.50;
-
- if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
- effectiveRatio *= 0.125;
-
- rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
-
- startRatio = effectiveRatio;
-
- previousIslandSize = islandSize; // log the last island size.
-
- if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
- firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
-
- islandSize = 1;
- }
+ if (islandSize < 7)
+ islandSize++; // island is still progressing, count size.
}
- else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
+ else
{
- // Begin counting island until we change speed again.
- firstDeltaSwitch = true;
+ if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
+ effectiveRatio *= 0.125;
+
+ if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
+ effectiveRatio *= 0.25;
+
+ if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
+ effectiveRatio *= 0.25;
+
+ if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
+ effectiveRatio *= 0.50;
+
+ if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
+ effectiveRatio *= 0.125;
+
+ rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
+
startRatio = effectiveRatio;
+
+ previousIslandSize = islandSize; // log the last island size.
+
+ if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
+ firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
+
islandSize = 1;
}
}
+ else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
+ {
+ // Begin counting island until we change speed again.
+ firstDeltaSwitch = true;
+ startRatio = effectiveRatio;
+ islandSize = 1;
+ }
}
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
@@ -154,7 +156,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
if (strainTime < min_speed_bonus)
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
- double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance);
+ double travelDistance = osuPrevObj?.TravelDistance ?? 0;
+ double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance);
return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
}
diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs
index 7314021a14..5c6b907e42 100644
--- a/osu.Game.Rulesets.Osu/OsuInputManager.cs
+++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs
@@ -3,8 +3,10 @@
using System.Collections.Generic;
using System.ComponentModel;
+using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Framework.Input.StateChanges.Events;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu
@@ -39,6 +41,19 @@ namespace osu.Game.Rulesets.Osu
return base.Handle(e);
}
+ protected override bool HandleMouseTouchStateChange(TouchStateChangeEvent e)
+ {
+ if (!AllowUserCursorMovement)
+ {
+ // Still allow for forwarding of the "touch" part, but replace the positional data with that of the mouse.
+ // Primarily relied upon by the "autopilot" osu! mod.
+ var touch = new Touch(e.Touch.Source, CurrentState.Mouse.Position);
+ e = new TouchStateChangeEvent(e.State, e.Input, touch, e.IsActive, null);
+ }
+
+ return base.HandleMouseTouchStateChange(e);
+ }
+
private class OsuKeyBindingContainer : RulesetKeyBindingContainer
{
public bool AllowUserPresses = true;
diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist
index 5fe822946a..ac658cd14e 100644
--- a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist
@@ -32,5 +32,7 @@
XSAppIconAssets
Assets.xcassets/AppIcon.appiconset
+ CADisableMinimumFrameDurationOnPhone
+
diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
index 861b800038..16be20f7f3 100644
--- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
@@ -12,7 +12,6 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
-using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK;
@@ -149,9 +148,6 @@ namespace osu.Game.Rulesets.Taiko.UI
centreHit.Colour = colours.Pink;
}
- [Resolved(canBeNull: true)]
- private GameplayClock gameplayClock { get; set; }
-
public bool OnPressed(KeyBindingPressEvent e)
{
Drawable target = null;
diff --git a/osu.Game.Tests.iOS/Info.plist b/osu.Game.Tests.iOS/Info.plist
index 98a4223116..1a89345bc5 100644
--- a/osu.Game.Tests.iOS/Info.plist
+++ b/osu.Game.Tests.iOS/Info.plist
@@ -32,5 +32,7 @@
XSAppIconAssets
Assets.xcassets/AppIcon.appiconset
+ CADisableMinimumFrameDurationOnPhone
+
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
index 6e5a546e87..81d89359e0 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
@@ -2,14 +2,20 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko;
using osu.Game.Scoring;
@@ -21,6 +27,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
[TestFixture]
public class LegacyScoreDecoderTest
{
+ private CultureInfo originalCulture;
+
+ [SetUp]
+ public void SetUp()
+ {
+ originalCulture = CultureInfo.CurrentCulture;
+ }
+
[Test]
public void TestDecodeManiaReplay()
{
@@ -44,6 +58,59 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestCultureInvariance()
+ {
+ var ruleset = new OsuRuleset().RulesetInfo;
+ var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
+ var beatmap = new TestBeatmap(ruleset);
+ var score = new Score
+ {
+ ScoreInfo = scoreInfo,
+ Replay = new Replay
+ {
+ Frames = new List
+ {
+ new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
+ }
+ }
+ };
+
+ // the "se" culture is used here, as it encodes the negative number sign as U+2212 MINUS SIGN,
+ // rather than the classic ASCII U+002D HYPHEN-MINUS.
+ CultureInfo.CurrentCulture = new CultureInfo("se");
+
+ var encodeStream = new MemoryStream();
+
+ var encoder = new LegacyScoreEncoder(score, beatmap);
+ encoder.Encode(encodeStream);
+
+ var decodeStream = new MemoryStream(encodeStream.GetBuffer());
+
+ var decoder = new TestLegacyScoreDecoder();
+ var decodedAfterEncode = decoder.Parse(decodeStream);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(decodedAfterEncode, Is.Not.Null);
+
+ Assert.That(decodedAfterEncode.ScoreInfo.User.Username, Is.EqualTo(scoreInfo.User.Username));
+ Assert.That(decodedAfterEncode.ScoreInfo.BeatmapInfoID, Is.EqualTo(scoreInfo.BeatmapInfoID));
+ Assert.That(decodedAfterEncode.ScoreInfo.Ruleset, Is.EqualTo(scoreInfo.Ruleset));
+ Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(scoreInfo.TotalScore));
+ Assert.That(decodedAfterEncode.ScoreInfo.MaxCombo, Is.EqualTo(scoreInfo.MaxCombo));
+ Assert.That(decodedAfterEncode.ScoreInfo.Date, Is.EqualTo(scoreInfo.Date));
+
+ Assert.That(decodedAfterEncode.Replay.Frames.Count, Is.EqualTo(1));
+ });
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ CultureInfo.CurrentCulture = originalCulture;
+ }
+
private class TestLegacyScoreDecoder : LegacyScoreDecoder
{
private static readonly Dictionary rulesets = new Ruleset[]
diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
index 6e2b9d20a8..6d0d5702e9 100644
--- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
@@ -1022,7 +1022,7 @@ namespace osu.Game.Tests.Beatmaps.IO
{
return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
{
- OnlineScoreID = 2,
+ OnlineID = 2,
BeatmapInfo = beatmapInfo,
BeatmapInfoID = beatmapInfo.ID
}, new ImportScoreTest.TestArchiveReader());
diff --git a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs
index 2a60a7b96d..3a82cbc785 100644
--- a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs
+++ b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs
@@ -25,9 +25,6 @@ namespace osu.Game.Tests.Beatmaps
private BeatmapSetInfo importedSet;
- [Resolved]
- private BeatmapManager beatmaps { get; set; }
-
private TestBeatmapDifficultyCache difficultyCache;
private IBindable starDifficultyBindable;
diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs
index d2193350ad..e47e24021f 100644
--- a/osu.Game.Tests/Database/BeatmapImporterTests.cs
+++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs
@@ -809,7 +809,7 @@ namespace osu.Game.Tests.Database
// TODO: reimplement when we have score support in realm.
// return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
// {
- // OnlineScoreID = 2,
+ // OnlineID = 2,
// Beatmap = beatmap,
// BeatmapInfoID = beatmap.ID
// }, new ImportScoreTest.TestArchiveReader());
@@ -852,7 +852,11 @@ namespace osu.Game.Tests.Database
{
IQueryable? resultSets = null;
- waitForOrAssert(() => (resultSets = realm.All().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any(),
+ waitForOrAssert(() =>
+ {
+ realm.Refresh();
+ return (resultSets = realm.All().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any();
+ },
@"BeatmapSet did not import to the database in allocated time.", timeout);
// ensure we were stored to beatmap database backing...
@@ -865,16 +869,16 @@ namespace osu.Game.Tests.Database
// ReSharper disable once PossibleUnintendedReferenceComparison
IEnumerable queryBeatmaps() => realm.All().Where(s => s.BeatmapSet != null && s.BeatmapSet == set);
- waitForOrAssert(() => queryBeatmaps().Count() == 12, @"Beatmaps did not import to the database in allocated time", timeout);
- waitForOrAssert(() => queryBeatmapSets().Count() == 1, @"BeatmapSet did not import to the database in allocated time", timeout);
+ Assert.AreEqual(12, queryBeatmaps().Count(), @"Beatmap count was not correct");
+ Assert.AreEqual(1, queryBeatmapSets().Count(), @"Beatmapset count was not correct");
- int countBeatmapSetBeatmaps = 0;
- int countBeatmaps = 0;
+ int countBeatmapSetBeatmaps;
+ int countBeatmaps;
- waitForOrAssert(() =>
- (countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) ==
- (countBeatmaps = queryBeatmaps().Count()),
- $@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout);
+ Assert.AreEqual(
+ countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count,
+ countBeatmaps = queryBeatmaps().Count(),
+ $@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).");
foreach (RealmBeatmap b in set.Beatmaps)
Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID));
diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs
index 3e8b6091fd..2285b22a3a 100644
--- a/osu.Game.Tests/Database/GeneralUsageTests.cs
+++ b/osu.Game.Tests/Database/GeneralUsageTests.cs
@@ -5,6 +5,8 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
+using osu.Game.Database;
+using osu.Game.Models;
#nullable enable
@@ -33,6 +35,39 @@ namespace osu.Game.Tests.Database
});
}
+ ///
+ /// Test to ensure that a `CreateContext` call nested inside a subscription doesn't cause any deadlocks
+ /// due to context fetching semaphores.
+ ///
+ [Test]
+ public void TestNestedContextCreationWithSubscription()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ bool callbackRan = false;
+
+ using (var context = realmFactory.CreateContext())
+ {
+ var subscription = context.All().QueryAsyncWithNotifications((sender, changes, error) =>
+ {
+ using (realmFactory.CreateContext())
+ {
+ callbackRan = true;
+ }
+ });
+
+ // Force the callback above to run.
+ using (realmFactory.CreateContext())
+ {
+ }
+
+ subscription?.Dispose();
+ }
+
+ Assert.IsTrue(callbackRan);
+ });
+ }
+
[Test]
public void TestBlockOperationsWithContention()
{
diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs
index 16e2c0fc6a..06cb5a3607 100644
--- a/osu.Game.Tests/Database/RealmLiveTests.cs
+++ b/osu.Game.Tests/Database/RealmLiveTests.cs
@@ -6,6 +6,7 @@ using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
+using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Models;
using Realms;
@@ -21,14 +22,41 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealm((realmFactory, _) =>
{
- ILive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive();
+ ILive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(realmFactory);
- ILive beatmap2 = realmFactory.CreateContext().All().First().ToLive();
+ ILive beatmap2 = realmFactory.CreateContext().All().First().ToLive(realmFactory);
Assert.AreEqual(beatmap, beatmap2);
});
}
+ [Test]
+ public void TestAccessAfterStorageMigrate()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
+
+ ILive liveBeatmap;
+
+ using (var context = realmFactory.CreateContext())
+ {
+ context.Write(r => r.Add(beatmap));
+
+ liveBeatmap = beatmap.ToLive(realmFactory);
+ }
+
+ using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target"))
+ {
+ migratedStorage.DeleteDirectory(string.Empty);
+
+ storage.Migrate(migratedStorage);
+
+ Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
+ }
+ });
+ }
+
[Test]
public void TestAccessAfterAttach()
{
@@ -36,7 +64,7 @@ namespace osu.Game.Tests.Database
{
var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
- var liveBeatmap = beatmap.ToLive();
+ var liveBeatmap = beatmap.ToLive(realmFactory);
using (var context = realmFactory.CreateContext())
context.Write(r => r.Add(beatmap));
@@ -49,7 +77,7 @@ namespace osu.Game.Tests.Database
public void TestAccessNonManaged()
{
var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
- var liveBeatmap = beatmap.ToLive();
+ var liveBeatmap = beatmap.ToLiveUnmanaged();
Assert.IsFalse(beatmap.Hidden);
Assert.IsFalse(liveBeatmap.Value.Hidden);
@@ -62,43 +90,6 @@ namespace osu.Game.Tests.Database
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
}
- [Test]
- public void TestValueAccessWithOpenContext()
- {
- RunTestWithRealm((realmFactory, _) =>
- {
- ILive? liveBeatmap = null;
- Task.Factory.StartNew(() =>
- {
- using (var threadContext = realmFactory.CreateContext())
- {
- var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
-
- liveBeatmap = beatmap.ToLive();
- }
- }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
-
- Debug.Assert(liveBeatmap != null);
-
- Task.Factory.StartNew(() =>
- {
- Assert.DoesNotThrow(() =>
- {
- using (realmFactory.CreateContext())
- {
- var resolved = liveBeatmap.Value;
-
- Assert.IsTrue(resolved.Realm.IsClosed);
- Assert.IsTrue(resolved.IsValid);
-
- // can access properties without a crash.
- Assert.IsFalse(resolved.Hidden);
- }
- });
- }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
- });
- }
-
[Test]
public void TestScopedReadWithoutContext()
{
@@ -111,7 +102,7 @@ namespace osu.Game.Tests.Database
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
- liveBeatmap = beatmap.ToLive();
+ liveBeatmap = beatmap.ToLive(realmFactory);
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
@@ -140,7 +131,7 @@ namespace osu.Game.Tests.Database
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
- liveBeatmap = beatmap.ToLive();
+ liveBeatmap = beatmap.ToLive(realmFactory);
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
@@ -154,6 +145,60 @@ namespace osu.Game.Tests.Database
});
}
+ [Test]
+ public void TestValueAccessNonManaged()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
+ var liveBeatmap = beatmap.ToLive(realmFactory);
+
+ Assert.DoesNotThrow(() =>
+ {
+ var __ = liveBeatmap.Value;
+ });
+ });
+ }
+
+ [Test]
+ public void TestValueAccessWithOpenContextFails()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ ILive? liveBeatmap = null;
+
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive(realmFactory);
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ // Can't be used, without a valid context.
+ Assert.Throws(() =>
+ {
+ var __ = liveBeatmap.Value;
+ });
+
+ // Can't be used, even from within a valid context.
+ using (realmFactory.CreateContext())
+ {
+ Assert.Throws(() =>
+ {
+ var __ = liveBeatmap.Value;
+ });
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
[Test]
public void TestValueAccessWithoutOpenContextFails()
{
@@ -166,7 +211,7 @@ namespace osu.Game.Tests.Database
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
- liveBeatmap = beatmap.ToLive();
+ liveBeatmap = beatmap.ToLive(realmFactory);
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
@@ -191,7 +236,7 @@ namespace osu.Game.Tests.Database
using (var updateThreadContext = realmFactory.CreateContext())
{
- updateThreadContext.All().SubscribeForNotifications(gotChange);
+ updateThreadContext.All().QueryAsyncWithNotifications(gotChange);
ILive? liveBeatmap = null;
Task.Factory.StartNew(() =>
@@ -205,7 +250,7 @@ namespace osu.Game.Tests.Database
// not just a refresh from the resolved Live.
threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
- liveBeatmap = beatmap.ToLive();
+ liveBeatmap = beatmap.ToLive(realmFactory);
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
@@ -215,23 +260,22 @@ namespace osu.Game.Tests.Database
Assert.AreEqual(0, updateThreadContext.All().Count());
Assert.AreEqual(0, changesTriggered);
- var resolved = liveBeatmap.Value;
-
- // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
- Assert.AreEqual(2, updateThreadContext.All().Count());
- Assert.AreEqual(1, changesTriggered);
-
- // even though the realm that this instance was resolved for was closed, it's still valid.
- Assert.IsTrue(resolved.Realm.IsClosed);
- Assert.IsTrue(resolved.IsValid);
-
- // can access properties without a crash.
- Assert.IsFalse(resolved.Hidden);
-
- updateThreadContext.Write(r =>
+ liveBeatmap.PerformRead(resolved =>
{
- // can use with the main context.
- r.Remove(resolved);
+ // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
+ // ReSharper disable once AccessToDisposedClosure
+ Assert.AreEqual(2, updateThreadContext.All().Count());
+ Assert.AreEqual(1, changesTriggered);
+
+ // can access properties without a crash.
+ Assert.IsFalse(resolved.Hidden);
+
+ // ReSharper disable once AccessToDisposedClosure
+ updateThreadContext.Write(r =>
+ {
+ // can use with the main context.
+ r.Remove(resolved);
+ });
});
}
diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs
index 04c9f2577a..6904464485 100644
--- a/osu.Game.Tests/Database/RealmTest.cs
+++ b/osu.Game.Tests/Database/RealmTest.cs
@@ -10,6 +10,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Database;
+using osu.Game.IO;
using osu.Game.Models;
#nullable enable
@@ -27,15 +28,16 @@ namespace osu.Game.Tests.Database
storage.DeleteDirectory(string.Empty);
}
- protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "")
+ protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "")
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
{
host.Run(new RealmTestGame(() =>
{
- var testStorage = storage.GetStorageForDirectory(caller);
+ // ReSharper disable once AccessToDisposedClosure
+ var testStorage = new OsuStorage(host, storage.GetStorageForDirectory(caller));
- using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ using (var realmFactory = new RealmContextFactory(testStorage, "client"))
{
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
testAction(realmFactory, testStorage);
@@ -58,7 +60,7 @@ namespace osu.Game.Tests.Database
{
var testStorage = storage.GetStorageForDirectory(caller);
- using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ using (var realmFactory = new RealmContextFactory(testStorage, "client"))
{
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
await testAction(realmFactory, testStorage);
diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs
index f4e0838be1..cc7e8a0c97 100644
--- a/osu.Game.Tests/Database/RulesetStoreTests.cs
+++ b/osu.Game.Tests/Database/RulesetStoreTests.cs
@@ -45,9 +45,9 @@ namespace osu.Game.Tests.Database
{
var rulesets = new RealmRulesetStore(realmFactory, storage);
- Assert.IsTrue((rulesets.AvailableRulesets.First() as RealmRuleset)?.IsManaged == false);
- Assert.IsTrue((rulesets.GetRuleset(0) as RealmRuleset)?.IsManaged == false);
- Assert.IsTrue((rulesets.GetRuleset("mania") as RealmRuleset)?.IsManaged == false);
+ Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged);
+ Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged);
+ Assert.IsFalse(rulesets.GetRuleset("mania")?.IsManaged);
});
}
}
diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
index 860828ae81..f05d9ab3dc 100644
--- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
+++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
@@ -52,6 +52,45 @@ namespace osu.Game.Tests.Database
Assert.That(queryCount(GlobalAction.Select), Is.EqualTo(2));
}
+ [Test]
+ public void TestDefaultsPopulationRemovesExcess()
+ {
+ Assert.That(queryCount(), Is.EqualTo(0));
+
+ KeyBindingContainer testContainer = new TestKeyBindingContainer();
+
+ // Add some excess bindings for an action which only supports 1.
+ using (var realm = realmContextFactory.CreateContext())
+ using (var transaction = realm.BeginWrite())
+ {
+ realm.Add(new RealmKeyBinding
+ {
+ Action = GlobalAction.Back,
+ KeyCombination = new KeyCombination(InputKey.A)
+ });
+
+ realm.Add(new RealmKeyBinding
+ {
+ Action = GlobalAction.Back,
+ KeyCombination = new KeyCombination(InputKey.S)
+ });
+
+ realm.Add(new RealmKeyBinding
+ {
+ Action = GlobalAction.Back,
+ KeyCombination = new KeyCombination(InputKey.D)
+ });
+
+ transaction.Commit();
+ }
+
+ Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(3));
+
+ keyBindingStore.Register(testContainer, Enumerable.Empty());
+
+ Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(1));
+ }
+
private int queryCount(GlobalAction? match = null)
{
using (var realm = realmContextFactory.CreateContext())
diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index 3bf6aaac7a..3aab28886e 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -15,6 +15,7 @@ using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Audio;
+using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@@ -167,7 +168,7 @@ namespace osu.Game.Tests.Gameplay
private class TestSkin : LegacySkin
{
public TestSkin(string resourceName, IStorageResourceProvider resources)
- : base(DefaultLegacySkin.Info, new TestResourceStore(resourceName), resources, "skin.ini")
+ : base(DefaultLegacySkin.CreateInfo(), new TestResourceStore(resourceName), resources, "skin.ini")
{
}
}
@@ -220,6 +221,7 @@ namespace osu.Game.Tests.Gameplay
public AudioManager AudioManager => Audio;
public IResourceStore Files => null;
public new IResourceStore Resources => base.Resources;
+ public RealmContextFactory RealmContextFactory => null;
public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null;
#endregion
diff --git a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs
index b612899d79..28937b2120 100644
--- a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs
+++ b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs
@@ -18,9 +18,6 @@ namespace osu.Game.Tests.Input
[Resolved]
private FrameworkConfigManager frameworkConfigManager { get; set; }
- [Resolved]
- private OsuConfigManager osuConfigManager { get; set; }
-
[TestCase(WindowMode.Windowed)]
[TestCase(WindowMode.Borderless)]
public void TestDisableConfining(WindowMode windowMode)
diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
index e458e66ab7..ae8eec2629 100644
--- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
+++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
@@ -3,12 +3,14 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
+using osu.Game.Utils;
namespace osu.Game.Tests.NonVisual
{
@@ -20,8 +22,10 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator().CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(1, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) }
+ }, combinations);
}
[Test]
@@ -29,9 +33,11 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(2, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModA);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModA) }
+ }, combinations);
}
[Test]
@@ -39,14 +45,13 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(4, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModA);
- Assert.IsTrue(combinations[2] is MultiMod);
- Assert.IsTrue(combinations[3] is ModB);
-
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModA) },
+ new[] { typeof(ModA), typeof(ModB) },
+ new[] { typeof(ModB) }
+ }, combinations);
}
[Test]
@@ -54,10 +59,12 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(3, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModA);
- Assert.IsTrue(combinations[2] is ModIncompatibleWithA);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModA) },
+ new[] { typeof(ModIncompatibleWithA) }
+ }, combinations);
}
[Test]
@@ -65,22 +72,17 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(8, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModA);
- Assert.IsTrue(combinations[2] is MultiMod);
- Assert.IsTrue(combinations[3] is ModB);
- Assert.IsTrue(combinations[4] is MultiMod);
- Assert.IsTrue(combinations[5] is ModIncompatibleWithA);
- Assert.IsTrue(combinations[6] is MultiMod);
- Assert.IsTrue(combinations[7] is ModIncompatibleWithAAndB);
-
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
- Assert.IsTrue(((MultiMod)combinations[4]).Mods[0] is ModB);
- Assert.IsTrue(((MultiMod)combinations[4]).Mods[1] is ModIncompatibleWithA);
- Assert.IsTrue(((MultiMod)combinations[6]).Mods[0] is ModIncompatibleWithA);
- Assert.IsTrue(((MultiMod)combinations[6]).Mods[1] is ModIncompatibleWithAAndB);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModA) },
+ new[] { typeof(ModA), typeof(ModB) },
+ new[] { typeof(ModB) },
+ new[] { typeof(ModB), typeof(ModIncompatibleWithA) },
+ new[] { typeof(ModIncompatibleWithA) },
+ new[] { typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB) },
+ new[] { typeof(ModIncompatibleWithAAndB) },
+ }, combinations);
}
[Test]
@@ -88,10 +90,12 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(3, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModAofA);
- Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModAofA) },
+ new[] { typeof(ModIncompatibleWithAofA) }
+ }, combinations);
}
[Test]
@@ -99,17 +103,13 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(4, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModA);
- Assert.IsTrue(combinations[2] is MultiMod);
- Assert.IsTrue(combinations[3] is MultiMod);
-
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[2] is ModC);
- Assert.IsTrue(((MultiMod)combinations[3]).Mods[0] is ModB);
- Assert.IsTrue(((MultiMod)combinations[3]).Mods[1] is ModC);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModA) },
+ new[] { typeof(ModA), typeof(ModB), typeof(ModC) },
+ new[] { typeof(ModB), typeof(ModC) }
+ }, combinations);
}
[Test]
@@ -117,13 +117,12 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(3, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModA);
- Assert.IsTrue(combinations[2] is MultiMod);
-
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModB);
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModA) },
+ new[] { typeof(ModB), typeof(ModIncompatibleWithA) }
+ }, combinations);
}
[Test]
@@ -131,13 +130,28 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(3, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModA);
- Assert.IsTrue(combinations[2] is MultiMod);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModA) },
+ new[] { typeof(ModA), typeof(ModB) }
+ }, combinations);
+ }
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
+ private void assertCombinations(Type[][] expectedCombinations, Mod[] actualCombinations)
+ {
+ Assert.AreEqual(expectedCombinations.Length, actualCombinations.Length);
+
+ Assert.Multiple(() =>
+ {
+ for (int i = 0; i < expectedCombinations.Length; ++i)
+ {
+ Type[] expectedTypes = expectedCombinations[i];
+ Type[] actualTypes = ModUtils.FlattenMod(actualCombinations[i]).Select(m => m.GetType()).ToArray();
+
+ Assert.That(expectedTypes, Is.EquivalentTo(actualTypes));
+ }
+ });
}
private class ModA : Mod
diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
index 840ff20a83..bc0041e2c2 100644
--- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
+++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
@@ -16,6 +16,27 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
[HeadlessTest]
public class StatefulMultiplayerClientTest : MultiplayerTestScene
{
+ [Test]
+ public void TestUserAddedOnJoin()
+ {
+ var user = new APIUser { Id = 33 };
+
+ AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3);
+ AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
+ }
+
+ [Test]
+ public void TestUserRemovedOnLeave()
+ {
+ var user = new APIUser { Id = 44 };
+
+ AddStep("add user", () => Client.AddUser(user));
+ AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
+
+ AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3);
+ AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1);
+ }
+
[Test]
public void TestPlayingUserTracking()
{
@@ -24,8 +45,6 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddRepeatStep("add some users", () => Client.AddUser(new APIUser { Id = id++ }), 5);
checkPlayingUserCount(0);
- AddAssert("playlist item is available", () => Client.CurrentMatchPlayingItem.Value != null);
-
changeState(3, MultiplayerUserState.WaitingForLoad);
checkPlayingUserCount(3);
@@ -43,8 +62,6 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddStep("leave room", () => Client.LeaveRoom());
checkPlayingUserCount(0);
-
- AddAssert("playlist item is null", () => Client.CurrentMatchPlayingItem.Value == null);
}
[Test]
diff --git a/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs
new file mode 100644
index 0000000000..2ec5b778d1
--- /dev/null
+++ b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs
@@ -0,0 +1,90 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Online.Chat;
+
+namespace osu.Game.Tests.Online.Chat
+{
+ [TestFixture]
+ public class MessageNotifierTest
+ {
+ [Test]
+ public void TestContainsUsernameMidlinePositive()
+ {
+ Assert.IsTrue(MessageNotifier.CheckContainsUsername("This is a test message", "Test"));
+ }
+
+ [Test]
+ public void TestContainsUsernameStartOfLinePositive()
+ {
+ Assert.IsTrue(MessageNotifier.CheckContainsUsername("Test message", "Test"));
+ }
+
+ [Test]
+ public void TestContainsUsernameEndOfLinePositive()
+ {
+ Assert.IsTrue(MessageNotifier.CheckContainsUsername("This is a test", "Test"));
+ }
+
+ [Test]
+ public void TestContainsUsernameMidlineNegative()
+ {
+ Assert.IsFalse(MessageNotifier.CheckContainsUsername("This is a testmessage for notifications", "Test"));
+ }
+
+ [Test]
+ public void TestContainsUsernameStartOfLineNegative()
+ {
+ Assert.IsFalse(MessageNotifier.CheckContainsUsername("Testmessage", "Test"));
+ }
+
+ [Test]
+ public void TestContainsUsernameEndOfLineNegative()
+ {
+ Assert.IsFalse(MessageNotifier.CheckContainsUsername("This is a notificationtest", "Test"));
+ }
+
+ [Test]
+ public void TestContainsUsernameBetweenInterpunction()
+ {
+ Assert.IsTrue(MessageNotifier.CheckContainsUsername("Hello 'test'-message", "Test"));
+ }
+
+ [Test]
+ public void TestContainsUsernameUnicode()
+ {
+ Assert.IsTrue(MessageNotifier.CheckContainsUsername("Test \u0460\u0460 message", "\u0460\u0460"));
+ }
+
+ [Test]
+ public void TestContainsUsernameUnicodeNegative()
+ {
+ Assert.IsFalse(MessageNotifier.CheckContainsUsername("Test ha\u0460\u0460o message", "\u0460\u0460"));
+ }
+
+ [Test]
+ public void TestContainsUsernameSpecialCharactersPositive()
+ {
+ Assert.IsTrue(MessageNotifier.CheckContainsUsername("Test [#^-^#] message", "[#^-^#]"));
+ }
+
+ [Test]
+ public void TestContainsUsernameSpecialCharactersNegative()
+ {
+ Assert.IsFalse(MessageNotifier.CheckContainsUsername("Test pad[#^-^#]oru message", "[#^-^#]"));
+ }
+
+ [Test]
+ public void TestContainsUsernameAtSign()
+ {
+ Assert.IsTrue(MessageNotifier.CheckContainsUsername("@username hi", "username"));
+ }
+
+ [Test]
+ public void TestContainsUsernameColon()
+ {
+ Assert.IsTrue(MessageNotifier.CheckContainsUsername("username: hi", "username"));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
index 8378b33b3d..4b160e1d67 100644
--- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
@@ -13,7 +13,6 @@ using osu.Game.Online.Solo;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
@@ -94,7 +93,7 @@ namespace osu.Game.Tests.Online
[Test]
public void TestDeserialiseSubmittableScoreWithEmptyMods()
{
- var score = new SubmittableScore(new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo });
+ var score = new SubmittableScore(new ScoreInfo());
var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(score));
@@ -106,7 +105,6 @@ namespace osu.Game.Tests.Online
{
var score = new SubmittableScore(new ScoreInfo
{
- Ruleset = new OsuRuleset().RulesetInfo,
Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } }
});
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index 24824b1e23..239c787349 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -114,18 +114,23 @@ namespace osu.Game.Tests.Online
public void TestTrackerRespectsChecksum()
{
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
+ AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).Wait());
+ addAvailabilityCheckStep("initially locally available", BeatmapAvailability.LocallyAvailable);
AddStep("import altered beatmap", () =>
{
beatmaps.Import(TestResources.GetTestBeatmapForImport(true)).Wait();
});
- addAvailabilityCheckStep("state still not downloaded", BeatmapAvailability.NotDownloaded);
+ addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded);
AddStep("recreate tracker", () => Child = availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
{
SelectedItem = { BindTarget = selectedItem }
});
addAvailabilityCheckStep("state not downloaded as well", BeatmapAvailability.NotDownloaded);
+
+ AddStep("reimport original beatmap", () => beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait());
+ addAvailabilityCheckStep("locally available after re-import", BeatmapAvailability.LocallyAvailable);
}
private void addAvailabilityCheckStep(string description, Func expected)
diff --git a/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs
deleted file mode 100644
index 5a621ecf84..0000000000
--- a/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using NUnit.Framework;
-using osu.Framework.Testing;
-using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Tests.Visual.Multiplayer;
-
-namespace osu.Game.Tests.OnlinePlay
-{
- [HeadlessTest]
- public class StatefulMultiplayerClientTest : MultiplayerTestScene
- {
- [Test]
- public void TestUserAddedOnJoin()
- {
- var user = new APIUser { Id = 33 };
-
- AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3);
- AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
- }
-
- [Test]
- public void TestUserRemovedOnLeave()
- {
- var user = new APIUser { Id = 44 };
-
- AddStep("add user", () => Client.AddUser(user));
- AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
-
- AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3);
- AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1);
- }
- }
-}
diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index 440d5e701f..445394fc77 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Text;
using System.Threading;
using NUnit.Framework;
@@ -12,8 +13,12 @@ using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
namespace osu.Game.Tests.Resources
{
@@ -137,5 +142,63 @@ namespace osu.Game.Tests.Resources
}
}
}
+
+ ///
+ /// Create a test score model.
+ ///
+ /// The ruleset for which the score was set against.
+ ///
+ public static ScoreInfo CreateTestScoreInfo(RulesetInfo ruleset = null) =>
+ CreateTestScoreInfo(CreateTestBeatmapSetInfo(1, new[] { ruleset ?? new OsuRuleset().RulesetInfo }).Beatmaps.First());
+
+ ///
+ /// Create a test score model.
+ ///
+ /// The beatmap for which the score was set against.
+ ///
+ public static ScoreInfo CreateTestScoreInfo(BeatmapInfo beatmap) => new ScoreInfo
+ {
+ User = new APIUser
+ {
+ Id = 2,
+ Username = "peppy",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ },
+ BeatmapInfo = beatmap,
+ Ruleset = beatmap.Ruleset,
+ RulesetID = beatmap.Ruleset.ID ?? 0,
+ Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() },
+ TotalScore = 2845370,
+ Accuracy = 0.95,
+ MaxCombo = 999,
+ Position = 1,
+ Rank = ScoreRank.S,
+ Date = DateTimeOffset.Now,
+ Statistics = new Dictionary
+ {
+ [HitResult.Miss] = 1,
+ [HitResult.Meh] = 50,
+ [HitResult.Ok] = 100,
+ [HitResult.Good] = 200,
+ [HitResult.Great] = 300,
+ [HitResult.Perfect] = 320,
+ [HitResult.SmallTickHit] = 50,
+ [HitResult.SmallTickMiss] = 25,
+ [HitResult.LargeTickHit] = 100,
+ [HitResult.LargeTickMiss] = 50,
+ [HitResult.SmallBonus] = 10,
+ [HitResult.SmallBonus] = 50
+ },
+ };
+
+ private class TestModHardRock : ModHardRock
+ {
+ public override double ScoreMultiplier => 1;
+ }
+
+ private class TestModDoubleTime : ModDoubleTime
+ {
+ public override double ScoreMultiplier => 1;
+ }
}
}
diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
index 0dee0f89ea..bbc92b7817 100644
--- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
+++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Tests.Scores.IO
Combo = 250,
User = new APIUser { Username = "Test user" },
Date = DateTimeOffset.Now,
- OnlineScoreID = 12345,
+ OnlineID = 12345,
};
var imported = await LoadScoreIntoOsu(osu, toImport);
@@ -52,7 +52,7 @@ namespace osu.Game.Tests.Scores.IO
Assert.AreEqual(toImport.Combo, imported.Combo);
Assert.AreEqual(toImport.User.Username, imported.User.Username);
Assert.AreEqual(toImport.Date, imported.Date);
- Assert.AreEqual(toImport.OnlineScoreID, imported.OnlineScoreID);
+ Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
}
finally
{
@@ -163,12 +163,12 @@ namespace osu.Game.Tests.Scores.IO
{
var osu = LoadOsuIntoHost(host, true);
- await LoadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader());
+ await LoadScoreIntoOsu(osu, new ScoreInfo { OnlineID = 2 }, new TestArchiveReader());
var scoreManager = osu.Dependencies.Get();
// Note: A new score reference is used here since the import process mutates the original object to set an ID
- Assert.That(scoreManager.IsAvailableLocally(new ScoreInfo { OnlineScoreID = 2 }));
+ Assert.That(scoreManager.IsAvailableLocally(new ScoreInfo { OnlineID = 2 }));
}
finally
{
diff --git a/osu.Game.Tests/Scores/IO/TestScoreEquality.cs b/osu.Game.Tests/Scores/IO/TestScoreEquality.cs
index d1374eb6e5..42fcb3acab 100644
--- a/osu.Game.Tests/Scores/IO/TestScoreEquality.cs
+++ b/osu.Game.Tests/Scores/IO/TestScoreEquality.cs
@@ -44,24 +44,6 @@ namespace osu.Game.Tests.Scores.IO
Assert.That(score1, Is.EqualTo(score2));
}
- [Test]
- public void TestNonMatchingByHash()
- {
- ScoreInfo score1 = new ScoreInfo { Hash = "a" };
- ScoreInfo score2 = new ScoreInfo { Hash = "b" };
-
- Assert.That(score1, Is.Not.EqualTo(score2));
- }
-
- [Test]
- public void TestMatchingByHash()
- {
- ScoreInfo score1 = new ScoreInfo { Hash = "a" };
- ScoreInfo score2 = new ScoreInfo { Hash = "a" };
-
- Assert.That(score1, Is.EqualTo(score2));
- }
-
[Test]
public void TestNonMatchingByNull()
{
diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
index ecc9c92025..f2ce002650 100644
--- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
+++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform;
+using osu.Game.Database;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Skinning;
@@ -163,32 +164,109 @@ namespace osu.Game.Tests.Skins.IO
assertCorrectMetadata(import2, "name 1 [my custom skin 2]", "author 1", osu);
});
+ [Test]
+ public Task TestExportThenImportDefaultSkin() => runSkinTest(osu =>
+ {
+ var skinManager = osu.Dependencies.Get();
+
+ skinManager.EnsureMutableSkin();
+
+ MemoryStream exportStream = new MemoryStream();
+
+ Guid originalSkinId = skinManager.CurrentSkinInfo.Value.ID;
+
+ skinManager.CurrentSkinInfo.Value.PerformRead(s =>
+ {
+ Assert.IsFalse(s.Protected);
+ Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType());
+
+ new LegacySkinExporter(osu.Dependencies.Get()).ExportModelTo(s, exportStream);
+
+ Assert.Greater(exportStream.Length, 0);
+ });
+
+ var imported = skinManager.Import(new ImportTask(exportStream, "exported.osk"));
+
+ imported.Result.PerformRead(s =>
+ {
+ Assert.IsFalse(s.Protected);
+ Assert.AreNotEqual(originalSkinId, s.ID);
+ Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType());
+ });
+
+ return Task.CompletedTask;
+ });
+
+ [Test]
+ public Task TestExportThenImportClassicSkin() => runSkinTest(osu =>
+ {
+ var skinManager = osu.Dependencies.Get();
+
+ skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo;
+
+ skinManager.EnsureMutableSkin();
+
+ MemoryStream exportStream = new MemoryStream();
+
+ Guid originalSkinId = skinManager.CurrentSkinInfo.Value.ID;
+
+ skinManager.CurrentSkinInfo.Value.PerformRead(s =>
+ {
+ Assert.IsFalse(s.Protected);
+ Assert.AreEqual(typeof(DefaultLegacySkin), s.CreateInstance(skinManager).GetType());
+
+ new LegacySkinExporter(osu.Dependencies.Get()).ExportModelTo(s, exportStream);
+
+ Assert.Greater(exportStream.Length, 0);
+ });
+
+ var imported = skinManager.Import(new ImportTask(exportStream, "exported.osk"));
+
+ imported.Result.PerformRead(s =>
+ {
+ Assert.IsFalse(s.Protected);
+ Assert.AreNotEqual(originalSkinId, s.ID);
+ Assert.AreEqual(typeof(DefaultLegacySkin), s.CreateInstance(skinManager).GetType());
+ });
+
+ return Task.CompletedTask;
+ });
+
#endregion
- private void assertCorrectMetadata(SkinInfo import1, string name, string creator, OsuGameBase osu)
+ private void assertCorrectMetadata(ILive import1, string name, string creator, OsuGameBase osu)
{
- Assert.That(import1.Name, Is.EqualTo(name));
- Assert.That(import1.Creator, Is.EqualTo(creator));
+ import1.PerformRead(i =>
+ {
+ Assert.That(i.Name, Is.EqualTo(name));
+ Assert.That(i.Creator, Is.EqualTo(creator));
- // for extra safety let's reconstruct the skin, reading from the skin.ini.
- var instance = import1.CreateInstance((IStorageResourceProvider)osu.Dependencies.Get(typeof(SkinManager)));
+ // for extra safety let's reconstruct the skin, reading from the skin.ini.
+ var instance = i.CreateInstance((IStorageResourceProvider)osu.Dependencies.Get(typeof(SkinManager)));
- Assert.That(instance.Configuration.SkinInfo.Name, Is.EqualTo(name));
- Assert.That(instance.Configuration.SkinInfo.Creator, Is.EqualTo(creator));
+ Assert.That(instance.Configuration.SkinInfo.Name, Is.EqualTo(name));
+ Assert.That(instance.Configuration.SkinInfo.Creator, Is.EqualTo(creator));
+ });
}
- private void assertImportedBoth(SkinInfo import1, SkinInfo import2)
+ private void assertImportedBoth(ILive import1, ILive import2)
{
- Assert.That(import2.ID, Is.Not.EqualTo(import1.ID));
- Assert.That(import2.Hash, Is.Not.EqualTo(import1.Hash));
- Assert.That(import2.Files.Select(f => f.FileInfoID), Is.Not.EquivalentTo(import1.Files.Select(f => f.FileInfoID)));
+ import1.PerformRead(i1 => import2.PerformRead(i2 =>
+ {
+ Assert.That(i2.ID, Is.Not.EqualTo(i1.ID));
+ Assert.That(i2.Hash, Is.Not.EqualTo(i1.Hash));
+ Assert.That(i2.Files.First(), Is.Not.EqualTo(i1.Files.First()));
+ }));
}
- private void assertImportedOnce(SkinInfo import1, SkinInfo import2)
+ private void assertImportedOnce(ILive import1, ILive import2)
{
- Assert.That(import2.ID, Is.EqualTo(import1.ID));
- Assert.That(import2.Hash, Is.EqualTo(import1.Hash));
- Assert.That(import2.Files.Select(f => f.FileInfoID), Is.EquivalentTo(import1.Files.Select(f => f.FileInfoID)));
+ import1.PerformRead(i1 => import2.PerformRead(i2 =>
+ {
+ Assert.That(i2.ID, Is.EqualTo(i1.ID));
+ Assert.That(i2.Hash, Is.EqualTo(i1.Hash));
+ Assert.That(i2.Files.First(), Is.EqualTo(i1.Files.First()));
+ }));
}
private MemoryStream createEmptyOsk()
@@ -255,10 +333,10 @@ namespace osu.Game.Tests.Skins.IO
}
}
- private async Task loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null)
+ private async Task> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null)
{
var skinManager = osu.Dependencies.Get();
- return (await skinManager.Import(archive)).Value;
+ return await skinManager.Import(archive);
}
}
}
diff --git a/osu.Game.Tests/Skins/TestSceneSkinResources.cs b/osu.Game.Tests/Skins/TestSceneSkinResources.cs
index 10f1ab31df..09535b76e3 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinResources.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinResources.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Tests.Skins
private void load()
{
var imported = skins.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-skin.osk"))).Result;
- skin = skins.GetSkin(imported.Value);
+ skin = imported.PerformRead(skinInfo => skins.GetSkin(skinInfo));
}
[Test]
diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
index ec16578b71..844fe7705a 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
@@ -5,15 +5,20 @@ using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Graphics.Textures;
using osu.Framework.Screens;
using osu.Framework.Testing;
+using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Database;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens;
using osu.Game.Screens.Backgrounds;
using osu.Game.Skinning;
+using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Background
{
@@ -21,8 +26,7 @@ namespace osu.Game.Tests.Visual.Background
public class TestSceneBackgroundScreenDefault : OsuTestScene
{
private BackgroundScreenStack stack;
- private BackgroundScreenDefault screen;
-
+ private TestBackgroundScreenDefault screen;
private Graphics.Backgrounds.Background getCurrentBackground() => screen.ChildrenOfType().FirstOrDefault();
[Resolved]
@@ -35,10 +39,96 @@ namespace osu.Game.Tests.Visual.Background
public void SetUpSteps()
{
AddStep("create background stack", () => Child = stack = new BackgroundScreenStack());
- AddStep("push default screen", () => stack.Push(screen = new BackgroundScreenDefault(false)));
+ AddStep("push default screen", () => stack.Push(screen = new TestBackgroundScreenDefault()));
AddUntilStep("wait for screen to load", () => screen.IsCurrentScreen());
}
+ [Test]
+ public void TestBeatmapBackgroundTracksBeatmap()
+ {
+ setSupporter(true);
+ setSourceMode(BackgroundSource.Beatmap);
+
+ AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
+ AddAssert("background changed", () => screen.CheckLastLoadChange() == true);
+
+ Graphics.Backgrounds.Background last = null;
+
+ AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground));
+ AddStep("store background", () => last = getCurrentBackground());
+
+ AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
+
+ AddUntilStep("wait for beatmap background to change", () => screen.CheckLastLoadChange() == true);
+
+ AddUntilStep("background is new beatmap background", () => last != getCurrentBackground());
+ AddStep("store background", () => last = getCurrentBackground());
+
+ AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
+
+ AddUntilStep("wait for beatmap background to change", () => screen.CheckLastLoadChange() == true);
+ AddUntilStep("background is new beatmap background", () => last != getCurrentBackground());
+ }
+
+ [Test]
+ public void TestBeatmapBackgroundTracksBeatmapWhenSuspended()
+ {
+ setSupporter(true);
+ setSourceMode(BackgroundSource.Beatmap);
+
+ AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
+ AddAssert("background changed", () => screen.CheckLastLoadChange() == true);
+ AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground));
+
+ BackgroundScreenBeatmap nestedScreen = null;
+
+ // of note, this needs to be a type that doesn't match BackgroundScreenDefault else it is silently not pushed by the background stack.
+ AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value)));
+ AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen());
+ AddUntilStep("previous background hidden", () => !screen.IsAlive);
+
+ AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null);
+
+ AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
+
+ AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null);
+
+ AddStep("pop screen back to top level", () => screen.MakeCurrent());
+
+ AddAssert("top level background changed", () => screen.CheckLastLoadChange() == true);
+ }
+
+ [Test]
+ public void TestBeatmapBackgroundIgnoresNoChangeWhenSuspended()
+ {
+ BackgroundScreenBeatmap nestedScreen = null;
+ WorkingBeatmap originalWorking = null;
+
+ setSupporter(true);
+ setSourceMode(BackgroundSource.Beatmap);
+
+ AddStep("change beatmap", () => originalWorking = Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
+ AddAssert("background changed", () => screen.CheckLastLoadChange() == true);
+ AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground));
+
+ // of note, this needs to be a type that doesn't match BackgroundScreenDefault else it is silently not pushed by the background stack.
+ AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value)));
+ AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen());
+
+ // we're testing a case where scheduling may be used to avoid issues, so ensure the scheduler is no longer running.
+ AddUntilStep("wait for top level not alive", () => !screen.IsAlive);
+
+ AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
+ AddStep("change beatmap back", () => Beatmap.Value = originalWorking);
+
+ AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null);
+
+ AddStep("pop screen back to top level", () => screen.MakeCurrent());
+
+ AddStep("top level screen is current", () => screen.IsCurrentScreen());
+ AddAssert("top level background reused existing", () => screen.CheckLastLoadChange() == false);
+ }
+
[Test]
public void TestBackgroundTypeSwitch()
{
@@ -77,36 +167,24 @@ namespace osu.Game.Tests.Visual.Background
[TestCase(BackgroundSource.Skin, typeof(SkinBackground))]
public void TestBackgroundDoesntReloadOnNoChange(BackgroundSource source, Type backgroundType)
{
- Graphics.Backgrounds.Background last = null;
-
setSourceMode(source);
setSupporter(true);
if (source == BackgroundSource.Skin)
setCustomSkin();
- AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground())?.GetType() == backgroundType);
+ AddUntilStep("wait for beatmap background to be loaded", () => (getCurrentBackground())?.GetType() == backgroundType);
AddAssert("next doesn't load new background", () => screen.Next() == false);
-
- // doesn't really need to be checked but might as well.
- AddWaitStep("wait a bit", 5);
- AddUntilStep("ensure same background instance", () => last == getCurrentBackground());
}
[Test]
public void TestBackgroundCyclingOnDefaultSkin([Values] bool supporter)
{
- Graphics.Backgrounds.Background last = null;
-
setSourceMode(BackgroundSource.Skin);
setSupporter(supporter);
setDefaultSkin();
- AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground())?.GetType() == typeof(Graphics.Backgrounds.Background));
+ AddUntilStep("wait for beatmap background to be loaded", () => (getCurrentBackground())?.GetType() == typeof(Graphics.Backgrounds.Background));
AddAssert("next cycles background", () => screen.Next());
-
- // doesn't really need to be checked but might as well.
- AddWaitStep("wait a bit", 5);
- AddUntilStep("ensure different background instance", () => last != getCurrentBackground());
}
private void setSourceMode(BackgroundSource source) =>
@@ -119,10 +197,46 @@ namespace osu.Game.Tests.Visual.Background
Id = API.LocalUser.Value.Id + 1,
});
+ private WorkingBeatmap createTestWorkingBeatmapWithUniqueBackground() => new UniqueBackgroundTestWorkingBeatmap(Audio);
+
+ private class TestBackgroundScreenDefault : BackgroundScreenDefault
+ {
+ private bool? lastLoadTriggerCausedChange;
+
+ public TestBackgroundScreenDefault()
+ : base(false)
+ {
+ }
+
+ public override bool Next()
+ {
+ bool didChange = base.Next();
+ lastLoadTriggerCausedChange = didChange;
+ return didChange;
+ }
+
+ public bool? CheckLastLoadChange()
+ {
+ bool? lastChange = lastLoadTriggerCausedChange;
+ lastLoadTriggerCausedChange = null;
+ return lastChange;
+ }
+ }
+
+ private class UniqueBackgroundTestWorkingBeatmap : TestWorkingBeatmap
+ {
+ public UniqueBackgroundTestWorkingBeatmap(AudioManager audioManager)
+ : base(new Beatmap(), null, audioManager)
+ {
+ }
+
+ protected override Texture GetBackground() => new Texture(1, 1);
+ }
+
private void setCustomSkin()
{
// feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin.
- AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo { ID = 5 });
+ AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo().ToLiveUnmanaged());
}
private void setDefaultSkin() => AddStep("set default skin", () => skins.CurrentSkinInfo.SetDefault());
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
index 194341d1ab..33b1d9a67d 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
@@ -18,7 +18,6 @@ using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
@@ -28,7 +27,6 @@ using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
-using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Graphics;
@@ -229,12 +227,7 @@ namespace osu.Game.Tests.Visual.Background
FadeAccessibleResults results = null;
- AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo
- {
- User = new APIUser { Username = "osu!" },
- BeatmapInfo = new TestBeatmap(Ruleset.Value).BeatmapInfo,
- Ruleset = Ruleset.Value,
- })));
+ AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(TestResources.CreateTestScoreInfo())));
AddUntilStep("Wait for results is current", () => results.IsCurrentScreen());
diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs
index 5effc1f215..0b9857486a 100644
--- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs
+++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs
@@ -11,17 +11,18 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
+using osu.Game.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osuTK;
-using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Tests.Visual.Beatmaps
{
- public class TestSceneBeatmapCard : OsuTestScene
+ public class TestSceneBeatmapCard : OsuManualInputManagerTestScene
{
///
/// All cards on this scene use a common online ID to ensure that map download, preview tracks, etc. can be tested manually with online sources.
@@ -227,7 +228,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
new BasicScrollContainer
{
RelativeSizeAxes = Axes.Both,
- Child = new FillFlowContainer
+ Child = new ReverseChildIDFillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@@ -248,6 +249,35 @@ namespace osu.Game.Tests.Visual.Beatmaps
}
[Test]
- public void TestNormal() => createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo));
+ public void TestNormal()
+ {
+ createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo));
+ }
+
+ [Test]
+ public void TestHoverState()
+ {
+ AddStep("create cards", () => Child = createContent(OverlayColourScheme.Blue, s => new BeatmapCard(s)));
+
+ AddStep("Hover card", () => InputManager.MoveMouseTo(firstCard()));
+ AddWaitStep("wait for potential state change", 5);
+ AddAssert("card is not expanded", () => !firstCard().Expanded.Value);
+
+ AddStep("Hover spectrum display", () => InputManager.MoveMouseTo(firstCard().ChildrenOfType().Single()));
+ AddUntilStep("card is expanded", () => firstCard().Expanded.Value);
+
+ AddStep("Hover difficulty content", () => InputManager.MoveMouseTo(firstCard().ChildrenOfType().Single()));
+ AddWaitStep("wait for potential state change", 5);
+ AddAssert("card is still expanded", () => firstCard().Expanded.Value);
+
+ AddStep("Hover main content again", () => InputManager.MoveMouseTo(firstCard()));
+ AddWaitStep("wait for potential state change", 5);
+ AddAssert("card is still expanded", () => firstCard().Expanded.Value);
+
+ AddStep("Hover away", () => InputManager.MoveMouseTo(this.ChildrenOfType().Last()));
+ AddUntilStep("card is not expanded", () => !firstCard().Expanded.Value);
+
+ BeatmapCard firstCard() => this.ChildrenOfType().First();
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs
new file mode 100644
index 0000000000..aec75884d6
--- /dev/null
+++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs
@@ -0,0 +1,71 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps.Drawables.Cards;
+using osu.Game.Graphics;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays;
+
+namespace osu.Game.Tests.Visual.Beatmaps
+{
+ public class TestSceneBeatmapCardDifficultyList : OsuTestScene
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ var beatmapSet = new APIBeatmapSet
+ {
+ Beatmaps = new[]
+ {
+ new APIBeatmap { RulesetID = 1, StarRating = 5.76, DifficultyName = "Oni" },
+ new APIBeatmap { RulesetID = 1, StarRating = 3.20, DifficultyName = "Muzukashii" },
+ new APIBeatmap { RulesetID = 1, StarRating = 2.45, DifficultyName = "Futsuu" },
+
+ new APIBeatmap { RulesetID = 0, StarRating = 2.04, DifficultyName = "Normal" },
+ new APIBeatmap { RulesetID = 0, StarRating = 3.51, DifficultyName = "Hard" },
+ new APIBeatmap { RulesetID = 0, StarRating = 5.25, DifficultyName = "Insane" },
+
+ new APIBeatmap { RulesetID = 2, StarRating = 2.64, DifficultyName = "Salad" },
+ new APIBeatmap { RulesetID = 2, StarRating = 3.56, DifficultyName = "Platter" },
+ new APIBeatmap { RulesetID = 2, StarRating = 4.65, DifficultyName = "Rain" },
+
+ new APIBeatmap { RulesetID = 3, StarRating = 1.93, DifficultyName = "[7K] Normal" },
+ new APIBeatmap { RulesetID = 3, StarRating = 3.18, DifficultyName = "[7K] Hyper" },
+ new APIBeatmap { RulesetID = 3, StarRating = 4.82, DifficultyName = "[7K] Another" },
+
+ new APIBeatmap { RulesetID = 4, StarRating = 9.99, DifficultyName = "Unknown?!" },
+ }
+ };
+
+ Child = new Container
+ {
+ Width = 300,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background2
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding(10),
+ Child = new BeatmapCardDifficultyList(beatmapSet)
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
index c81a1abfbc..c23db5e440 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
@@ -10,6 +10,7 @@ using osu.Game.Beatmaps;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Beatmaps.IO;
@@ -89,6 +90,7 @@ namespace osu.Game.Tests.Visual.Editing
confirmEditingBeatmap(() => targetDifficulty);
AddAssert("no objects selected", () => !EditorBeatmap.SelectedHitObjects.Any());
+ AddUntilStep("wait for drawable ruleset", () => Editor.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
AddStep("paste object", () => Editor.Paste());
if (sameRuleset)
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
index 92c8131568..db20d3c7ba 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Editing
public void TestCreateNewBeatmap()
{
AddStep("save beatmap", () => Editor.Save());
- AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0);
+ AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.IsManaged);
AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == false);
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
index 160af47a6d..50794f15ed 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
@@ -9,6 +9,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
@@ -44,6 +45,7 @@ namespace osu.Game.Tests.Visual.Editing
protected override void LoadEditor()
{
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.RulesetID == 0));
+ SelectedMods.Value = new[] { new ModCinema() };
base.LoadEditor();
}
@@ -67,6 +69,7 @@ namespace osu.Game.Tests.Visual.Editing
var background = this.ChildrenOfType().Single();
return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0;
});
+ AddAssert("no mods selected", () => SelectedMods.Value.Count == 0);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
index 7398527f57..c5f56cae9e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
@@ -12,6 +12,7 @@ using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
@@ -41,7 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestEmptyLegacyBeatmapSkinFallsBack()
{
- CreateSkinTest(SkinInfo.Default, () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null));
+ CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null));
AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
}
@@ -52,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("setup skins", () =>
{
- skinManager.CurrentSkinInfo.Value = gameCurrentSkin;
+ skinManager.CurrentSkinInfo.Value = gameCurrentSkin.ToLiveUnmanaged();
currentBeatmapSkin = getBeatmapSkin();
});
});
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
index 745932315c..fa27e1abdd 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
@@ -26,6 +26,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("total number of results == 1", () =>
{
var score = new ScoreInfo();
+
((FailPlayer)Player).ScoreProcessor.PopulateScore(score);
return score.Statistics.Values.Sum() == 1;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
index f5f17a0bc1..e03c8d7561 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
@@ -85,11 +85,12 @@ namespace osu.Game.Tests.Visual.Gameplay
loopGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1);
var target = addEventToLoop ? loopGroup : sprite.TimelineGroup;
- target.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
+ double targetTime = addEventToLoop ? 20000 : 0;
+ target.Alpha.Add(Easing.None, targetTime + firstStoryboardEvent, targetTime + firstStoryboardEvent + 500, 0, 1);
// these should be ignored due to being in the future.
sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1);
- loopGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1);
+ loopGroup.Alpha.Add(Easing.None, 38000, 40000, 0, 1);
storyboard.GetLayer("Background").Add(sprite);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index 06eaa726c9..958d617d63 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -251,7 +251,12 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestMutedNotificationMuteButton()
{
- addVolumeSteps("mute button", () => volumeOverlay.IsMuted.Value = true, () => !volumeOverlay.IsMuted.Value);
+ addVolumeSteps("mute button", () =>
+ {
+ // Importantly, in the case the volume is muted but the user has a volume level set, it should be retained.
+ audioManager.VolumeTrack.Value = 0.5f;
+ volumeOverlay.IsMuted.Value = true;
+ }, () => !volumeOverlay.IsMuted.Value && audioManager.VolumeTrack.Value == 0.5f);
}
///
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
index f47fae33ca..42c4f89e9d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
@@ -164,7 +164,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private ScoreInfo getScoreInfo(bool replayAvailable)
{
- return new APIScoreInfo
+ return new APIScore
{
OnlineID = 2553163309,
RulesetID = 0,
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
index dcc193669b..e6361a15d7 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
@@ -43,83 +43,88 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty());
- [SetUp]
- public void SetUp() => Schedule(() =>
+ [SetUpSteps]
+ public void SetUpSteps()
{
- replay = new Replay();
+ AddStep("Reset recorder state", cleanUpState);
- Add(new GridContainer
+ AddStep("Setup containers", () =>
{
- RelativeSizeAxes = Axes.Both,
- Content = new[]
+ replay = new Replay();
+
+ Add(new GridContainer
{
- new Drawable[]
+ RelativeSizeAxes = Axes.Both,
+ Content = new[]
{
- recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
+ new Drawable[]
{
- Recorder = recorder = new TestReplayRecorder(new Score
+ recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
- Replay = replay,
- ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo }
- })
- {
- ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
- },
- Child = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
+ Recorder = recorder = new TestReplayRecorder(new Score
{
- new Box
+ Replay = replay,
+ ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo }
+ })
+ {
+ ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
+ },
+ Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
{
- Colour = Color4.Brown,
- RelativeSizeAxes = Axes.Both,
- },
- new OsuSpriteText
- {
- Text = "Recording",
- Scale = new Vector2(3),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- new TestInputConsumer()
- }
- },
- }
- },
- new Drawable[]
- {
- playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
+ new Box
+ {
+ Colour = Color4.Brown,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuSpriteText
+ {
+ Text = "Recording",
+ Scale = new Vector2(3),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ new TestInputConsumer()
+ }
+ },
+ }
+ },
+ new Drawable[]
{
- ReplayInputHandler = new TestFramedReplayInputHandler(replay)
+ playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
- GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
- },
- Child = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
+ ReplayInputHandler = new TestFramedReplayInputHandler(replay)
{
- new Box
+ GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
+ },
+ Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
{
- Colour = Color4.DarkBlue,
- RelativeSizeAxes = Axes.Both,
- },
- new OsuSpriteText
- {
- Text = "Playback",
- Scale = new Vector2(3),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- new TestInputConsumer()
- }
- },
+ new Box
+ {
+ Colour = Color4.DarkBlue,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuSpriteText
+ {
+ Text = "Playback",
+ Scale = new Vector2(3),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ new TestInputConsumer()
+ }
+ },
+ }
}
}
- }
+ });
});
- });
+ }
[Test]
public void TestBasic()
@@ -184,7 +189,14 @@ namespace osu.Game.Tests.Visual.Gameplay
[TearDownSteps]
public void TearDown()
{
- AddStep("stop recorder", () => recorder.Expire());
+ AddStep("stop recorder", cleanUpState);
+ }
+
+ private void cleanUpState()
+ {
+ // Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`.
+ recorder?.RemoveAndDisposeImmediately();
+ recorder = null;
}
public class TestFramedReplayInputHandler : FramedReplayInputHandler
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs
deleted file mode 100644
index 3f7155f1e2..0000000000
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs
+++ /dev/null
@@ -1,228 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Input.Bindings;
-using osu.Framework.Input.Events;
-using osu.Framework.Input.StateChanges;
-using osu.Game.Beatmaps;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Replays;
-using osu.Game.Rulesets;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Osu;
-using osu.Game.Rulesets.Replays;
-using osu.Game.Rulesets.UI;
-using osu.Game.Scoring;
-using osu.Game.Screens.Play;
-using osu.Game.Tests.Visual.UserInterface;
-using osuTK;
-using osuTK.Graphics;
-
-namespace osu.Game.Tests.Visual.Gameplay
-{
- public class TestSceneReplayRecording : OsuTestScene
- {
- private readonly TestRulesetInputManager playbackManager;
-
- private readonly TestRulesetInputManager recordingManager;
-
- [Cached]
- private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty());
-
- public TestSceneReplayRecording()
- {
- Replay replay = new Replay();
-
- Add(new GridContainer
- {
- RelativeSizeAxes = Axes.Both,
- Content = new[]
- {
- new Drawable[]
- {
- recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
- {
- Recorder = new TestReplayRecorder(new Score
- {
- Replay = replay,
- ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo }
- })
- {
- ScreenSpaceToGamefield = pos => recordingManager?.ToLocalSpace(pos) ?? Vector2.Zero,
- },
- Child = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- new Box
- {
- Colour = Color4.Brown,
- RelativeSizeAxes = Axes.Both,
- },
- new OsuSpriteText
- {
- Text = "Recording",
- Scale = new Vector2(3),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- new TestConsumer()
- }
- },
- }
- },
- new Drawable[]
- {
- playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
- {
- ReplayInputHandler = new TestFramedReplayInputHandler(replay)
- {
- GamefieldToScreenSpace = pos => playbackManager?.ToScreenSpace(pos) ?? Vector2.Zero,
- },
- Child = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- new Box
- {
- Colour = Color4.DarkBlue,
- RelativeSizeAxes = Axes.Both,
- },
- new OsuSpriteText
- {
- Text = "Playback",
- Scale = new Vector2(3),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- new TestConsumer()
- }
- },
- }
- }
- }
- });
- }
-
- protected override void Update()
- {
- base.Update();
-
- playbackManager.ReplayInputHandler.SetFrameFromTime(Time.Current - 500);
- }
- }
-
- public class TestFramedReplayInputHandler : FramedReplayInputHandler
- {
- public TestFramedReplayInputHandler(Replay replay)
- : base(replay)
- {
- }
-
- public override void CollectPendingInputs(List inputs)
- {
- inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
- inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() });
- }
- }
-
- public class TestConsumer : CompositeDrawable, IKeyBindingHandler
- {
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent.ReceivePositionalInputAt(screenSpacePos);
-
- private readonly Box box;
-
- public TestConsumer()
- {
- Size = new Vector2(30);
-
- Origin = Anchor.Centre;
-
- InternalChildren = new Drawable[]
- {
- box = new Box
- {
- Colour = Color4.Black,
- RelativeSizeAxes = Axes.Both,
- },
- };
- }
-
- protected override bool OnMouseMove(MouseMoveEvent e)
- {
- Position = e.MousePosition;
- return base.OnMouseMove(e);
- }
-
- public bool OnPressed(KeyBindingPressEvent e)
- {
- if (e.Repeat)
- return false;
-
- box.Colour = Color4.White;
- return true;
- }
-
- public void OnReleased(KeyBindingReleaseEvent e)
- {
- box.Colour = Color4.Black;
- }
- }
-
- public class TestRulesetInputManager : RulesetInputManager
- {
- public TestRulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
- : base(ruleset, variant, unique)
- {
- }
-
- protected override KeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
- => new TestKeyBindingContainer();
-
- internal class TestKeyBindingContainer : KeyBindingContainer
- {
- public override IEnumerable DefaultKeyBindings => new[]
- {
- new KeyBinding(InputKey.MouseLeft, TestAction.Down),
- };
- }
- }
-
- public class TestReplayFrame : ReplayFrame
- {
- public Vector2 Position;
-
- public List Actions = new List();
-
- public TestReplayFrame(double time, Vector2 position, params TestAction[] actions)
- : base(time)
- {
- Position = position;
- Actions.AddRange(actions);
- }
- }
-
- public enum TestAction
- {
- Down,
- }
-
- internal class TestReplayRecorder : ReplayRecorder
- {
- public TestReplayRecorder(Score target)
- : base(target)
- {
- }
-
- protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) =>
- new TestReplayFrame(Time.Current, mousePosition, actions.ToArray());
- }
-}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs
index a0b27755b7..a0602e21b9 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs
@@ -2,12 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
-using osu.Game.Skinning;
using osu.Game.Skinning.Editor;
namespace osu.Game.Tests.Visual.Gameplay
@@ -16,9 +14,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
private SkinEditor skinEditor;
- [Resolved]
- private SkinManager skinManager { get; set; }
-
protected override bool Autoplay => true;
[SetUpSteps]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
index 723e35ed55..3074a91dc6 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
@@ -10,7 +10,6 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
-using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
@@ -36,9 +35,6 @@ namespace osu.Game.Tests.Visual.Gameplay
private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First();
- [Resolved]
- private OsuConfigManager config { get; set; }
-
[Test]
public void TestComboCounterIncrementing()
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
index 5fbccd54c8..f7e9a1fe16 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
@@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestReplayRecorder recorder;
- private readonly ManualClock manualClock = new ManualClock();
+ private ManualClock manualClock;
private OsuSpriteText latencyDisplay;
@@ -66,113 +66,121 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty());
- [SetUp]
- public void SetUp() => Schedule(() =>
+ [SetUpSteps]
+ public void SetUpSteps()
{
- replay = new Replay();
+ AddStep("Reset recorder state", cleanUpState);
- users.BindTo(spectatorClient.PlayingUsers);
- users.BindCollectionChanged((obj, args) =>
+ AddStep("Setup containers", () =>
{
- switch (args.Action)
+ replay = new Replay();
+ manualClock = new ManualClock();
+
+ spectatorClient.OnNewFrames += onNewFrames;
+
+ users.BindTo(spectatorClient.PlayingUsers);
+ users.BindCollectionChanged((obj, args) =>
{
- case NotifyCollectionChangedAction.Add:
- Debug.Assert(args.NewItems != null);
-
- foreach (int user in args.NewItems)
- {
- if (user == api.LocalUser.Value.Id)
- spectatorClient.WatchUser(user);
- }
-
- break;
-
- case NotifyCollectionChangedAction.Remove:
- Debug.Assert(args.OldItems != null);
-
- foreach (int user in args.OldItems)
- {
- if (user == api.LocalUser.Value.Id)
- spectatorClient.StopWatchingUser(user);
- }
-
- break;
- }
- }, true);
-
- spectatorClient.OnNewFrames += onNewFrames;
-
- Add(new GridContainer
- {
- RelativeSizeAxes = Axes.Both,
- Content = new[]
- {
- new Drawable[]
+ switch (args.Action)
{
- recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
+ case NotifyCollectionChangedAction.Add:
+ Debug.Assert(args.NewItems != null);
+
+ foreach (int user in args.NewItems)
+ {
+ if (user == api.LocalUser.Value.Id)
+ spectatorClient.WatchUser(user);
+ }
+
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ Debug.Assert(args.OldItems != null);
+
+ foreach (int user in args.OldItems)
+ {
+ if (user == api.LocalUser.Value.Id)
+ spectatorClient.StopWatchingUser(user);
+ }
+
+ break;
+ }
+ }, true);
+
+ Children = new Drawable[]
+ {
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Content = new[]
{
- Recorder = recorder = new TestReplayRecorder
+ new Drawable[]
{
- ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
- },
- Child = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
+ recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
- new Box
+ Recorder = recorder = new TestReplayRecorder
+ {
+ ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
+ },
+ Child = new Container
{
- Colour = Color4.Brown,
RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = Color4.Brown,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuSpriteText
+ {
+ Text = "Sending",
+ Scale = new Vector2(3),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ new TestInputConsumer()
+ }
},
- new OsuSpriteText
- {
- Text = "Sending",
- Scale = new Vector2(3),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- new TestInputConsumer()
}
},
+ new Drawable[]
+ {
+ playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
+ {
+ Clock = new FramedClock(manualClock),
+ ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay)
+ {
+ GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
+ },
+ Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = Color4.DarkBlue,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuSpriteText
+ {
+ Text = "Receiving",
+ Scale = new Vector2(3),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ new TestInputConsumer()
+ }
+ },
+ }
+ }
}
},
- new Drawable[]
- {
- playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
- {
- Clock = new FramedClock(manualClock),
- ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay)
- {
- GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
- },
- Child = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- new Box
- {
- Colour = Color4.DarkBlue,
- RelativeSizeAxes = Axes.Both,
- },
- new OsuSpriteText
- {
- Text = "Receiving",
- Scale = new Vector2(3),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- new TestInputConsumer()
- }
- },
- }
- }
- }
+ latencyDisplay = new OsuSpriteText()
+ };
});
-
- Add(latencyDisplay = new OsuSpriteText());
- });
+ }
private void onNewFrames(int userId, FrameDataBundle frames)
{
@@ -189,6 +197,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestBasic()
{
+ AddStep("Wait for user input", () => { });
}
private double latency = SpectatorClient.TIME_BETWEEN_SENDS;
@@ -232,11 +241,15 @@ namespace osu.Game.Tests.Visual.Gameplay
[TearDownSteps]
public void TearDown()
{
- AddStep("stop recorder", () =>
- {
- recorder.Expire();
- spectatorClient.OnNewFrames -= onNewFrames;
- });
+ AddStep("stop recorder", cleanUpState);
+ }
+
+ private void cleanUpState()
+ {
+ // Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`.
+ recorder?.RemoveAndDisposeImmediately();
+ recorder = null;
+ spectatorClient.OnNewFrames -= onNewFrames;
}
public class TestFramedReplayInputHandler : FramedReplayInputHandler
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
index 57d60cea9e..c65595d82e 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Menus
private TestToolbar toolbar;
[Resolved]
- private RulesetStore rulesets { get; set; }
+ private IRulesetStore rulesets { get; set; }
[SetUp]
public void SetUp() => Schedule(() =>
diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs
index 357db16e2c..5acb44ac45 100644
--- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs
@@ -31,17 +31,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected BeatmapInfo InitialBeatmap { get; private set; }
protected BeatmapInfo OtherBeatmap { get; private set; }
- protected IScreen CurrentScreen => multiplayerScreenStack.CurrentScreen;
- protected IScreen CurrentSubScreen => multiplayerScreenStack.MultiplayerScreen.CurrentSubScreen;
+ protected IScreen CurrentScreen => multiplayerComponents.CurrentScreen;
+ protected IScreen CurrentSubScreen => multiplayerComponents.MultiplayerScreen.CurrentSubScreen;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
- private TestMultiplayerScreenStack multiplayerScreenStack;
+ private TestMultiplayerComponents multiplayerComponents;
- protected TestMultiplayerClient Client => multiplayerScreenStack.Client;
- protected TestMultiplayerRoomManager RoomManager => multiplayerScreenStack.RoomManager;
+ protected TestMultiplayerClient Client => multiplayerComponents.Client;
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
@@ -65,12 +64,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
OtherBeatmap = importedSet.Beatmaps.Last(b => b.RulesetID == 0);
});
- AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack()));
- AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded);
+ AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents()));
+ AddUntilStep("wait for multiplayer to load", () => multiplayerComponents.IsLoaded);
AddUntilStep("wait for lounge to load", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
- AddUntilStep("wait for lounge", () => multiplayerScreenStack.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
- AddStep("open room", () => multiplayerScreenStack.ChildrenOfType().Single().Open(new Room
+ AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
+ AddStep("open room", () => multiplayerComponents.ChildrenOfType().Single().Open(new Room
{
Name = { Value = "Test Room" },
QueueMode = { Value = Mode },
@@ -93,7 +92,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
- AddUntilStep("wait for join", () => RoomManager.RoomJoined);
+ AddUntilStep("wait for join", () => Client.RoomJoined);
}
[Test]
@@ -110,8 +109,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready);
clickReadyButton();
- AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player player && player.IsLoaded);
- AddStep("exit player", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent());
+ AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded);
+ AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());
}
private void clickReadyButton()
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs
index ccfae1deef..a5744f9986 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestFirstItemSelectedByDefault()
{
- AddAssert("first item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID);
+ AddAssert("first item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID);
}
[Test]
@@ -27,13 +27,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
addItem(() => OtherBeatmap);
AddAssert("playlist has 2 items", () => Client.APIRoom?.Playlist.Count == 2);
- AddAssert("last playlist item is different", () => Client.APIRoom?.Playlist[1].Beatmap.Value.OnlineID == OtherBeatmap.OnlineID);
addItem(() => InitialBeatmap);
AddAssert("playlist has 3 items", () => Client.APIRoom?.Playlist.Count == 3);
- AddAssert("last playlist item is different", () => Client.APIRoom?.Playlist[2].Beatmap.Value.OnlineID == InitialBeatmap.OnlineID);
- AddAssert("first item still selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID);
+ AddAssert("first item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID);
}
[Test]
@@ -43,7 +41,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("playlist has only one item", () => Client.APIRoom?.Playlist.Count == 1);
AddAssert("playlist item is expired", () => Client.APIRoom?.Playlist[0].Expired == true);
- AddAssert("last item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID);
+ AddAssert("last item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID);
}
[Test]
@@ -55,12 +53,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
RunGameplay();
AddAssert("first item expired", () => Client.APIRoom?.Playlist[0].Expired == true);
- AddAssert("next item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[1].ID);
+ AddAssert("next item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[1].ID);
RunGameplay();
AddAssert("second item expired", () => Client.APIRoom?.Playlist[1].Expired == true);
- AddAssert("next item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[2].ID);
+ AddAssert("next item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[2].ID);
}
[Test]
@@ -74,22 +72,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("change queue mode", () => Client.ChangeSettings(queueMode: QueueMode.HostOnly));
AddAssert("playlist has 3 items", () => Client.APIRoom?.Playlist.Count == 3);
- AddAssert("playlist item is the other beatmap", () => Client.CurrentMatchPlayingItem.Value?.BeatmapID == OtherBeatmap.OnlineID);
- AddAssert("playlist item is not expired", () => Client.APIRoom?.Playlist[1].Expired == false);
+ AddAssert("item 2 is not expired", () => Client.APIRoom?.Playlist[1].Expired == false);
+ AddAssert("current item is the other beatmap", () => Client.Room?.Settings.PlaylistItemId == 2);
}
[Test]
public void TestCorrectItemSelectedAfterNewItemAdded()
{
addItem(() => OtherBeatmap);
- AddAssert("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID);
+ AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID);
}
private void addItem(Func beatmap)
{
- AddStep("click edit button", () =>
+ AddStep("click add button", () =>
{
- InputManager.MoveMouseTo(this.ChildrenOfType().Single().AddOrEditPlaylistButton);
+ InputManager.MoveMouseTo(this.ChildrenOfType().Single());
InputManager.Click(MouseButton.Left);
});
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs
index 2c28a1752e..423822cbe4 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs
@@ -11,12 +11,14 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
+using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Tests.Beatmaps;
using osuTK;
@@ -172,6 +174,39 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha));
}
+ [Test]
+ public void TestMultiplayerRooms()
+ {
+ AddStep("create rooms", () => Child = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(5),
+ Children = new[]
+ {
+ new DrawableMatchRoom(new Room
+ {
+ Name = { Value = "A host-only room" },
+ QueueMode = { Value = QueueMode.HostOnly },
+ Type = { Value = MatchType.HeadToHead }
+ }),
+ new DrawableMatchRoom(new Room
+ {
+ Name = { Value = "An all-players, team-versus room" },
+ QueueMode = { Value = QueueMode.AllPlayers },
+ Type = { Value = MatchType.TeamVersus }
+ }),
+ new DrawableMatchRoom(new Room
+ {
+ Name = { Value = "A round-robin room" },
+ QueueMode = { Value = QueueMode.AllPlayersRoundRobin },
+ Type = { Value = MatchType.HeadToHead }
+ }),
+ }
+ });
+ }
+
private DrawableRoom createLoungeRoom(Room room)
{
room.Host.Value ??= new APIUser { Username = "peppy", Id = 2 };
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
index 55aa665ff1..f9784384fd 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . 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.Diagnostics;
using System.Linq;
@@ -48,7 +49,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestNonEditableNonSelectable()
{
- createPlaylist(false, false);
+ createPlaylist();
moveToItem(0);
assertHandleVisibility(0, false);
@@ -61,7 +62,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestEditable()
{
- createPlaylist(true, false);
+ createPlaylist(p =>
+ {
+ p.AllowReordering = true;
+ p.AllowDeletion = true;
+ });
moveToItem(0);
assertHandleVisibility(0, true);
@@ -74,7 +79,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestMarkInvalid()
{
- createPlaylist(true, true);
+ createPlaylist(p =>
+ {
+ p.AllowReordering = true;
+ p.AllowDeletion = true;
+ p.AllowSelection = true;
+ });
AddStep("mark item 0 as invalid", () => playlist.Items[0].MarkInvalid());
@@ -87,7 +97,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestSelectable()
{
- createPlaylist(false, true);
+ createPlaylist(p => p.AllowSelection = true);
moveToItem(0);
assertHandleVisibility(0, false);
@@ -101,7 +111,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestEditableSelectable()
{
- createPlaylist(true, true);
+ createPlaylist(p =>
+ {
+ p.AllowReordering = true;
+ p.AllowDeletion = true;
+ p.AllowSelection = true;
+ });
moveToItem(0);
assertHandleVisibility(0, true);
@@ -115,7 +130,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestSelectionNotLostAfterRearrangement()
{
- createPlaylist(true, true);
+ createPlaylist(p =>
+ {
+ p.AllowReordering = true;
+ p.AllowDeletion = true;
+ p.AllowSelection = true;
+ });
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
@@ -128,95 +148,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]);
}
- [Test]
- public void TestItemRemovedOnDeletion()
- {
- PlaylistItem selectedItem = null;
-
- createPlaylist(true, true);
-
- moveToItem(0);
- AddStep("click", () => InputManager.Click(MouseButton.Left));
- AddStep("retrieve selection", () => selectedItem = playlist.SelectedItem.Value);
-
- moveToDeleteButton(0);
- AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
-
- AddAssert("item removed", () => !playlist.Items.Contains(selectedItem));
- }
-
- [Test]
- public void TestNextItemSelectedAfterDeletion()
- {
- createPlaylist(true, true);
-
- moveToItem(0);
- AddStep("click", () => InputManager.Click(MouseButton.Left));
-
- moveToDeleteButton(0);
- AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
-
- AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
- }
-
- [Test]
- public void TestLastItemSelectedAfterLastItemDeleted()
- {
- createPlaylist(true, true);
-
- AddWaitStep("wait for flow", 5); // Items may take 1 update frame to flow. A wait count of 5 is guaranteed to result in the flow being updated as desired.
- AddStep("scroll to bottom", () => playlist.ChildrenOfType>().First().ScrollToEnd(false));
-
- moveToItem(19);
- AddStep("click", () => InputManager.Click(MouseButton.Left));
-
- moveToDeleteButton(19);
- AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
-
- AddAssert("item 18 is selected", () => playlist.SelectedItem.Value == playlist.Items[18]);
- }
-
- [Test]
- public void TestSelectionResetWhenAllItemsDeleted()
- {
- createPlaylist(true, true);
-
- AddStep("remove all but one item", () =>
- {
- playlist.Items.RemoveRange(1, playlist.Items.Count - 1);
- });
-
- moveToItem(0);
- AddStep("click", () => InputManager.Click(MouseButton.Left));
- moveToDeleteButton(0);
- AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
-
- AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
- }
-
- // Todo: currently not possible due to bindable list shortcomings (https://github.com/ppy/osu-framework/issues/3081)
- // [Test]
- public void TestNextItemSelectedAfterExternalDeletion()
- {
- createPlaylist(true, true);
-
- moveToItem(0);
- AddStep("click", () => InputManager.Click(MouseButton.Left));
- AddStep("remove item 0", () => playlist.Items.RemoveAt(0));
-
- AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
- }
-
- [Test]
- public void TestChangeBeatmapAndRemove()
- {
- createPlaylist(true, true);
-
- AddStep("change beatmap of first item", () => playlist.Items[0].BeatmapID = 30);
- moveToDeleteButton(0);
- AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
- }
-
[Test]
public void TestDownloadButtonHiddenWhenBeatmapExists()
{
@@ -224,7 +155,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("import beatmap", () => manager.Import(beatmap.BeatmapSet).Wait());
- createPlaylist(beatmap);
+ createPlaylistWithBeatmaps(beatmap);
assertDownloadButtonVisible(false);
@@ -247,7 +178,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
var byChecksum = CreateAPIBeatmap();
byChecksum.Checksum = "1337"; // Some random checksum that does not exist locally.
- createPlaylist(byOnlineId, byChecksum);
+ createPlaylistWithBeatmaps(byOnlineId, byChecksum);
AddAssert("download buttons shown", () => playlist.ChildrenOfType().All(d => d.IsPresent));
}
@@ -261,7 +192,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
beatmap.BeatmapSet.HasExplicitContent = true;
- createPlaylist(beatmap);
+ createPlaylistWithBeatmaps(beatmap);
}
[Test]
@@ -269,7 +200,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create playlist", () =>
{
- Child = playlist = new TestPlaylist(false, false)
+ Child = playlist = new TestPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -312,11 +243,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
[TestCase(true)]
public void TestWithOwner(bool withOwner)
{
- createPlaylist(false, false, withOwner);
+ createPlaylist(p => p.ShowItemOwners = withOwner);
AddAssert("owner visible", () => playlist.ChildrenOfType().All(a => a.IsPresent == withOwner));
}
+ [Test]
+ public void TestWithAllButtonsEnabled()
+ {
+ createPlaylist(p =>
+ {
+ p.AllowDeletion = true;
+ p.AllowShowingResults = true;
+ p.AllowEditing = true;
+ });
+ }
+
private void moveToItem(int index, Vector2? offset = null)
=> AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset));
@@ -326,12 +268,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.MoveMouseTo(item.ChildrenOfType.PlaylistItemHandle>().Single(), offset);
});
- private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () =>
- {
- var item = playlist.ChildrenOfType>().ElementAt(index);
- InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset);
- });
-
private void assertHandleVisibility(int index, bool visible)
=> AddAssert($"handle {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType.PlaylistItemHandle>().ElementAt(index).Alpha > 0) == visible);
@@ -340,17 +276,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible);
- private void createPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false)
+ private void createPlaylist(Action setupPlaylist = null)
{
AddStep("create playlist", () =>
{
- Child = playlist = new TestPlaylist(allowEdit, allowSelection, showItemOwner)
+ Child = playlist = new TestPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300)
};
+ setupPlaylist?.Invoke(playlist);
+
for (int i = 0; i < 20; i++)
{
playlist.Items.Add(new PlaylistItem
@@ -386,11 +324,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
}
- private void createPlaylist(params IBeatmapInfo[] beatmaps)
+ private void createPlaylistWithBeatmaps(params IBeatmapInfo[] beatmaps)
{
AddStep("create playlist", () =>
{
- Child = playlist = new TestPlaylist(false, false)
+ Child = playlist = new TestPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -423,11 +361,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private class TestPlaylist : DrawableRoomPlaylist
{
public new IReadOnlyDictionary> ItemMap => base.ItemMap;
-
- public TestPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false)
- : base(allowEdit, allowSelection, showItemOwner: showItemOwner)
- {
- }
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs
index 1de7289446..c7eeff81fe 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs
@@ -7,7 +7,9 @@ using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
+using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer;
+using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
@@ -19,7 +21,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestFirstItemSelectedByDefault()
{
- AddAssert("first item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID);
+ AddAssert("first item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID);
}
[Test]
@@ -27,7 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
selectNewItem(() => InitialBeatmap);
- AddAssert("playlist item still selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID);
+ AddAssert("playlist item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID);
}
[Test]
@@ -35,7 +37,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
selectNewItem(() => OtherBeatmap);
- AddAssert("playlist item still selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID);
+ AddAssert("playlist item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID);
}
[Test]
@@ -46,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("playlist contains two items", () => Client.APIRoom?.Playlist.Count == 2);
AddAssert("first playlist item expired", () => Client.APIRoom?.Playlist[0].Expired == true);
AddAssert("second playlist item not expired", () => Client.APIRoom?.Playlist[1].Expired == false);
- AddAssert("second playlist item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[1].ID);
+ AddAssert("second playlist item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[1].ID);
}
[Test]
@@ -74,11 +76,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("api room updated", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers);
}
+ [Test]
+ public void TestAddItemsAsHost()
+ {
+ addItem(() => OtherBeatmap);
+
+ AddAssert("playlist contains two items", () => Client.APIRoom?.Playlist.Count == 2);
+ }
+
private void selectNewItem(Func beatmap)
{
+ AddUntilStep("wait for playlist panels to load", () =>
+ {
+ var queueList = this.ChildrenOfType().Single();
+ return queueList.ChildrenOfType().Count() == queueList.Items.Count;
+ });
+
AddStep("click edit button", () =>
{
- InputManager.MoveMouseTo(this.ChildrenOfType().Single().AddOrEditPlaylistButton);
+ InputManager.MoveMouseTo(this.ChildrenOfType().First());
InputManager.Click(MouseButton.Left);
});
@@ -88,7 +104,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap()));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
- AddUntilStep("selected item is new beatmap", () => Client.CurrentMatchPlayingItem.Value?.Beatmap.Value?.OnlineID == otherBeatmap.OnlineID);
+ AddUntilStep("selected item is new beatmap", () => (CurrentSubScreen as MultiplayerMatchSubScreen)?.SelectedItem.Value?.BeatmapID == otherBeatmap.OnlineID);
+ }
+
+ private void addItem(Func beatmap)
+ {
+ AddStep("click add button", () =>
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().Single());
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
+ AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(beatmap()));
+ AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
index d66603a448..1d61a5d496 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
@@ -2,11 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Game.Beatmaps;
using osu.Game.Online.Rooms;
-using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Components;
@@ -18,12 +15,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene
{
- [Resolved]
- private BeatmapManager beatmapManager { get; set; }
-
- [Resolved]
- private RulesetStore rulesetStore { get; set; }
-
[SetUp]
public new void Setup() => Schedule(() =>
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 4521a7fa0f..bd0e5c4eb9 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -30,6 +30,8 @@ using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
+using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
+using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Spectate;
@@ -44,10 +46,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
- private TestMultiplayerScreenStack multiplayerScreenStack;
+ private TestMultiplayerComponents multiplayerComponents;
- private TestMultiplayerClient client => multiplayerScreenStack.Client;
- private TestMultiplayerRoomManager roomManager => multiplayerScreenStack.RoomManager;
+ private TestMultiplayerClient client => multiplayerComponents.Client;
+ private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager;
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
@@ -69,8 +71,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
});
- AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack()));
- AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded);
+ AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents()));
+ AddUntilStep("wait for multiplayer to load", () => multiplayerComponents.IsLoaded);
AddUntilStep("wait for lounge to load", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
}
@@ -214,7 +216,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Press select", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
- AddUntilStep("wait for join", () => roomManager.RoomJoined);
+ AddUntilStep("wait for join", () => client.RoomJoined);
}
[Test]
@@ -293,7 +295,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("join room", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
- AddUntilStep("wait for join", () => roomManager.RoomJoined);
+ AddUntilStep("wait for join", () => client.RoomJoined);
AddAssert("Check participant count correct", () => client.APIRoom?.ParticipantCount.Value == 1);
AddAssert("Check participant list contains user", () => client.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1);
@@ -351,7 +353,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick());
AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
- AddUntilStep("wait for join", () => roomManager.RoomJoined);
+ AddUntilStep("wait for join", () => client.RoomJoined);
}
[Test]
@@ -391,12 +393,51 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- AddStep("set user ready", () => client.ChangeState(MultiplayerUserState.Ready));
- AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
+ pressReadyButton();
+ AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle);
}
+ [Test]
+ public void TestPlayStartsWithCorrectBeatmapWhileAtSongSelect()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ pressReadyButton();
+
+ AddStep("Enter song select", () =>
+ {
+ var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
+ ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId);
+ });
+
+ AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true);
+
+ AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID);
+
+ AddStep("Select next beatmap", () => InputManager.Key(Key.Down));
+
+ AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != client.Room?.Playlist.First().BeatmapID);
+
+ AddStep("start match externally", () => client.StartMatch());
+
+ AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
+
+ AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID);
+ }
+
[Test]
public void TestLocalPlayDoesNotStartWhileSpectatingWithNoBeatmap()
{
@@ -432,7 +473,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("start match externally", () => client.StartMatch());
- AddAssert("play not started", () => multiplayerScreenStack.IsCurrentScreen());
+ AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen());
}
[Test]
@@ -476,7 +517,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
});
- AddUntilStep("play started", () => multiplayerScreenStack.CurrentScreen is SpectatorScreen);
+ AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is SpectatorScreen);
}
[Test]
@@ -518,16 +559,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("open mod overlay", () => this.ChildrenOfType().Single().TriggerClick());
- AddStep("invoke on back button", () => multiplayerScreenStack.OnBackButton());
+ AddStep("invoke on back button", () => multiplayerComponents.OnBackButton());
AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden);
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
- testLeave("back button", () => multiplayerScreenStack.OnBackButton());
+ testLeave("back button", () => multiplayerComponents.OnBackButton());
// mimics home button and OS window close
- testLeave("forced exit", () => multiplayerScreenStack.Exit());
+ testLeave("forced exit", () => multiplayerComponents.Exit());
void testLeave(string actionName, Action action)
{
@@ -555,20 +596,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value);
-
- AddStep("click ready button", () =>
- {
- InputManager.MoveMouseTo(readyButton);
- InputManager.Click(MouseButton.Left);
- });
-
- AddUntilStep("wait for player to be ready", () => client.Room?.Users[0].State == MultiplayerUserState.Ready);
- AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value);
-
- AddStep("click start button", () => InputManager.Click(MouseButton.Left));
-
- AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player);
+ enterGameplay();
// Gameplay runs in real-time, so we need to incrementally check if gameplay has finished in order to not time out.
for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000)
@@ -577,7 +605,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType().SingleOrDefault()?.GameplayClock.CurrentTime > time);
}
- AddUntilStep("wait for results", () => multiplayerScreenStack.CurrentScreen is ResultsScreen);
+ AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen);
}
[Test]
@@ -618,7 +646,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("join room", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
- AddUntilStep("wait for join", () => roomManager.RoomJoined);
+ AddUntilStep("wait for join", () => client.RoomJoined);
AddAssert("local room has correct settings", () =>
{
@@ -628,12 +656,178 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
- private MultiplayerReadyButton readyButton => this.ChildrenOfType().Single();
+ [Test]
+ public void TestSpectatingStateResetOnBackButtonDuringGameplay()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ QueueMode = { Value = QueueMode.AllPlayers },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ AddStep("set spectating state", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
+ AddUntilStep("state set to spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
+
+ AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
+ AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready));
+
+ pressReadyButton(1234);
+ AddUntilStep("wait for gameplay", () => (multiplayerComponents.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true);
+
+ AddStep("press back button and exit", () =>
+ {
+ multiplayerComponents.OnBackButton();
+ multiplayerComponents.Exit();
+ });
+
+ AddUntilStep("wait for return to match subscreen", () => multiplayerComponents.MultiplayerScreen.IsCurrentScreen());
+ AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle);
+ }
+
+ [Test]
+ public void TestSpectatingStateNotResetOnBackButtonOutsideOfGameplay()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ QueueMode = { Value = QueueMode.AllPlayers },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ AddStep("set spectating state", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
+ AddUntilStep("state set to spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
+
+ AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
+ AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready));
+
+ pressReadyButton(1234);
+ AddUntilStep("wait for gameplay", () => (multiplayerComponents.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true);
+ AddStep("set other user loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded));
+ AddStep("set other user finished play", () => client.ChangeUserState(1234, MultiplayerUserState.FinishedPlay));
+
+ AddStep("press back button and exit", () =>
+ {
+ multiplayerComponents.OnBackButton();
+ multiplayerComponents.Exit();
+ });
+
+ AddUntilStep("wait for return to match subscreen", () => multiplayerComponents.MultiplayerScreen.IsCurrentScreen());
+ AddWaitStep("wait for possible state change", 5);
+ AddUntilStep("user state is spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
+ }
+
+ [Test]
+ public void TestItemAddedByOtherUserDuringGameplay()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ QueueMode = { Value = QueueMode.AllPlayers },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ enterGameplay();
+
+ AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
+ AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem
+ {
+ BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1
+ })));
+
+ AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2);
+
+ AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());
+ AddUntilStep("queue contains item", () => this.ChildrenOfType().Single().Items.Single().ID == 2);
+ }
+
+ [Test]
+ public void TestItemAddedAndDeletedByOtherUserDuringGameplay()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ QueueMode = { Value = QueueMode.AllPlayers },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ enterGameplay();
+
+ AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
+ AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem
+ {
+ BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1
+ })));
+
+ AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2);
+
+ AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2));
+ AddUntilStep("item removed from playlist", () => client.Room?.Playlist.Count == 1);
+
+ AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());
+ AddUntilStep("queue is empty", () => this.ChildrenOfType().Single().Items.Count == 0);
+ }
+
+ private void enterGameplay()
+ {
+ pressReadyButton();
+ pressReadyButton();
+ AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player);
+ }
+
+ private ReadyButton readyButton => this.ChildrenOfType().Single();
+
+ private void pressReadyButton(int? playingUserId = null)
+ {
+ AddUntilStep("wait for ready button to be enabled", () => readyButton.Enabled.Value);
+
+ MultiplayerUserState lastState = MultiplayerUserState.Idle;
+ MultiplayerRoomUser user = null;
+
+ AddStep("click ready button", () =>
+ {
+ user = playingUserId == null ? client.LocalUser : client.Room?.Users.Single(u => u.UserID == playingUserId);
+ lastState = user?.State ?? MultiplayerUserState.Idle;
+
+ InputManager.MoveMouseTo(readyButton);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddUntilStep("wait for state change", () => user?.State != lastState);
+ }
private void createRoom(Func room)
{
- AddUntilStep("wait for lounge", () => multiplayerScreenStack.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
- AddStep("open room", () => multiplayerScreenStack.ChildrenOfType().Single().Open(room()));
+ AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
+ AddStep("open room", () => multiplayerComponents.ChildrenOfType().Single().Open(room()));
AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2);
@@ -644,7 +838,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
- AddUntilStep("wait for join", () => roomManager.RoomJoined);
+ AddUntilStep("wait for join", () => client.RoomJoined);
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 84b24ba3a1..d671673d3c 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -144,7 +144,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set mods", () => SelectedMods.Value = new[] { new TaikoModDoubleTime() });
AddStep("confirm selection", () => songSelect.FinaliseSelection());
- AddStep("exit song select", () => songSelect.Exit());
+
+ AddUntilStep("song select exited", () => !songSelect.IsCurrentScreen());
AddAssert("beatmap not changed", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap));
AddAssert("ruleset not changed", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo));
@@ -178,7 +179,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public new BeatmapCarousel Carousel => base.Carousel;
public TestMultiplayerMatchSongSelect(Room room, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
- : base(room, beatmap, ruleset)
+ : base(room, null, beatmap, ruleset)
{
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
index 5708b2f789..73f2ed5b39 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Multiplayer;
@@ -27,7 +28,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("initialise gameplay", () =>
{
- Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, Client.CurrentMatchPlayingItem.Value, Client.Room?.Users.ToArray()));
+ Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, new PlaylistItem
+ {
+ Beatmap = { Value = Beatmap.Value.BeatmapInfo },
+ Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset }
+ }, Client.Room?.Users.ToArray()));
});
AddUntilStep("wait for player to be current", () => player.IsCurrentScreen() && player.IsLoaded);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
new file mode 100644
index 0000000000..674ee0f186
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
@@ -0,0 +1,234 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Graphics;
+using osu.Framework.Platform;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
+using osu.Game.Rulesets;
+using osu.Game.Screens.OnlinePlay;
+using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Resources;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMultiplayerPlaylist : MultiplayerTestScene
+ {
+ private MultiplayerPlaylist list;
+ private BeatmapManager beatmaps;
+ private RulesetStore rulesets;
+ private BeatmapSetInfo importedSet;
+ private BeatmapInfo importedBeatmap;
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, AudioManager audio)
+ {
+ Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
+ Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+ }
+
+ [SetUp]
+ public new void Setup() => Schedule(() =>
+ {
+ Child = list = new MultiplayerPlaylist
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.4f, 0.8f)
+ };
+ });
+
+ [SetUpSteps]
+ public new void SetUpSteps()
+ {
+ AddStep("import beatmap", () =>
+ {
+ beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
+ importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
+ importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0);
+ });
+
+ AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }));
+ }
+
+ [Test]
+ public void TestNonExpiredItemsAddedToQueueList()
+ {
+ assertItemInQueueListStep(1, 0);
+
+ addItemStep();
+ assertItemInQueueListStep(2, 1);
+
+ addItemStep();
+ assertItemInQueueListStep(3, 2);
+ }
+
+ [Test]
+ public void TestExpiredItemsAddedToHistoryList()
+ {
+ assertItemInQueueListStep(1, 0);
+
+ addItemStep(true);
+ assertItemInHistoryListStep(2, 0);
+
+ addItemStep(true);
+ assertItemInHistoryListStep(3, 0);
+ assertItemInHistoryListStep(2, 1);
+
+ // Initial item is still in the queue.
+ assertItemInQueueListStep(1, 0);
+ }
+
+ [Test]
+ public void TestExpiredItemsMoveToQueueList()
+ {
+ addItemStep();
+ addItemStep();
+
+ AddStep("finish current item", () => Client.FinishCurrentItem());
+
+ assertItemInHistoryListStep(1, 0);
+ assertItemInQueueListStep(2, 0);
+ assertItemInQueueListStep(3, 1);
+
+ AddStep("finish current item", () => Client.FinishCurrentItem());
+
+ assertItemInHistoryListStep(2, 0);
+ assertItemInHistoryListStep(1, 1);
+ assertItemInQueueListStep(3, 0);
+
+ AddStep("finish current item", () => Client.FinishCurrentItem());
+
+ assertItemInHistoryListStep(3, 0);
+ assertItemInHistoryListStep(2, 1);
+ assertItemInHistoryListStep(1, 2);
+ }
+
+ [Test]
+ public void TestListsClearedWhenRoomLeft()
+ {
+ addItemStep();
+ AddStep("finish current item", () => Client.FinishCurrentItem());
+
+ AddStep("leave room", () => RoomManager.PartRoom());
+ AddUntilStep("wait for room part", () => Client.Room == null);
+
+ AddUntilStep("item 0 not in lists", () => !inHistoryList(0) && !inQueueList(0));
+ AddUntilStep("item 1 not in lists", () => !inHistoryList(0) && !inQueueList(0));
+ }
+
+ [Ignore("Expired items are initially removed from the room.")]
+ [Test]
+ public void TestJoinRoomWithMixedItemsAddedInCorrectLists()
+ {
+ AddStep("leave room", () => RoomManager.PartRoom());
+ AddUntilStep("wait for room part", () => Client.Room == null);
+
+ AddStep("join room with items", () =>
+ {
+ RoomManager.CreateRoom(new Room
+ {
+ Name = { Value = "test name" },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
+ Ruleset = { Value = Ruleset.Value }
+ },
+ new PlaylistItem
+ {
+ Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
+ Ruleset = { Value = Ruleset.Value },
+ Expired = true
+ }
+ }
+ });
+ });
+
+ AddUntilStep("wait for room join", () => RoomJoined);
+
+ assertItemInQueueListStep(1, 0);
+ assertItemInHistoryListStep(2, 0);
+ }
+
+ ///
+ /// Adds a step to create a new playlist item.
+ ///
+ private void addItemStep(bool expired = false) => AddStep("add item", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem
+ {
+ Beatmap = { Value = importedBeatmap },
+ BeatmapID = importedBeatmap.OnlineID ?? -1,
+ Expired = expired,
+ PlayedAt = DateTimeOffset.Now
+ })));
+
+ ///
+ /// Asserts the position of a given playlist item in the queue list.
+ ///
+ /// The item id.
+ /// The index at which the item should appear visually. The item with index 0 is at the top of the list.
+ private void assertItemInQueueListStep(int playlistItemId, int visualIndex)
+ {
+ changeDisplayModeStep(MultiplayerPlaylistDisplayMode.Queue);
+
+ AddUntilStep($"{playlistItemId} in queue at pos = {visualIndex}", () =>
+ {
+ return !inHistoryList(playlistItemId)
+ && this.ChildrenOfType()
+ .Single()
+ .ChildrenOfType()
+ .OrderBy(drawable => drawable.Position.Y)
+ .TakeWhile(drawable => drawable.Item.ID != playlistItemId)
+ .Count() == visualIndex;
+ });
+ }
+
+ ///
+ /// Asserts the position of a given playlist item in the history list.
+ ///
+ /// The item id.
+ /// The index at which the item should appear visually. The item with index 0 is at the top of the list.
+ private void assertItemInHistoryListStep(int playlistItemId, int visualIndex)
+ {
+ changeDisplayModeStep(MultiplayerPlaylistDisplayMode.History);
+
+ AddUntilStep($"{playlistItemId} in history at pos = {visualIndex}", () =>
+ {
+ return !inQueueList(playlistItemId)
+ && this.ChildrenOfType()
+ .Single()
+ .ChildrenOfType()
+ .OrderBy(drawable => drawable.Position.Y)
+ .TakeWhile(drawable => drawable.Item.ID != playlistItemId)
+ .Count() == visualIndex;
+ });
+ }
+
+ private void changeDisplayModeStep(MultiplayerPlaylistDisplayMode mode) => AddStep($"change list to {mode}", () => list.DisplayMode.Value = mode);
+
+ private bool inQueueList(int playlistItemId)
+ {
+ return this.ChildrenOfType()
+ .Single()
+ .Items.Any(i => i.ID == playlistItemId);
+ }
+
+ private bool inHistoryList(int playlistItemId)
+ {
+ return this.ChildrenOfType()
+ .Single()
+ .Items.Any(i => i.ID == playlistItemId);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
new file mode 100644
index 0000000000..61a92c32a4
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
@@ -0,0 +1,164 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Platform;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
+using osu.Game.Rulesets;
+using osu.Game.Screens.OnlinePlay;
+using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
+using osu.Game.Tests.Resources;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMultiplayerQueueList : MultiplayerTestScene
+ {
+ private readonly Bindable selectedItem = new Bindable();
+
+ [Cached(typeof(UserLookupCache))]
+ private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache();
+
+ private MultiplayerQueueList playlist;
+ private BeatmapManager beatmaps;
+ private RulesetStore rulesets;
+ private BeatmapSetInfo importedSet;
+ private BeatmapInfo importedBeatmap;
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, AudioManager audio)
+ {
+ Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
+ Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+ }
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("create playlist", () =>
+ {
+ selectedItem.Value = null;
+
+ Child = playlist = new MultiplayerQueueList
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(500, 300),
+ SelectedItem = { BindTarget = selectedItem },
+ Items = { BindTarget = Client.APIRoom!.Playlist }
+ };
+ });
+
+ AddStep("import beatmap", () =>
+ {
+ beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
+ importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
+ importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0);
+ });
+
+ AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }));
+ }
+
+ [Test]
+ public void TestDeleteButtonAlwaysVisibleForHost()
+ {
+ AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }));
+ AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers);
+
+ addPlaylistItem(() => API.LocalUser.Value.OnlineID);
+ assertDeleteButtonVisibility(1, true);
+ addPlaylistItem(() => 1234);
+ assertDeleteButtonVisibility(2, true);
+ }
+
+ [Test]
+ public void TestDeleteButtonOnlyVisibleForItemOwnerIfNotHost()
+ {
+ AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }));
+ AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers);
+
+ AddStep("join other user", () => Client.AddUser(new APIUser { Id = 1234 }));
+ AddStep("set other user as host", () => Client.TransferHost(1234));
+
+ addPlaylistItem(() => API.LocalUser.Value.OnlineID);
+ assertDeleteButtonVisibility(1, true);
+ addPlaylistItem(() => 1234);
+ assertDeleteButtonVisibility(2, false);
+
+ AddStep("set local user as host", () => Client.TransferHost(API.LocalUser.Value.OnlineID));
+ assertDeleteButtonVisibility(1, true);
+ assertDeleteButtonVisibility(2, true);
+ }
+
+ [Test]
+ public void TestCurrentItemDoesNotHaveDeleteButton()
+ {
+ AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }));
+ AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers);
+
+ addPlaylistItem(() => API.LocalUser.Value.OnlineID);
+
+ AddStep("select item 0", () => selectedItem.Value = playlist.ChildrenOfType>().ElementAt(0).Model);
+ assertDeleteButtonVisibility(0, false);
+ assertDeleteButtonVisibility(1, true);
+
+ AddStep("select item 1", () => selectedItem.Value = playlist.ChildrenOfType>().ElementAt(1).Model);
+ assertDeleteButtonVisibility(0, true);
+ assertDeleteButtonVisibility(1, false);
+ }
+
+ private void addPlaylistItem(Func userId)
+ {
+ long itemId = -1;
+
+ AddStep("add playlist item", () =>
+ {
+ MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem
+ {
+ Beatmap = { Value = importedBeatmap },
+ BeatmapID = importedBeatmap.OnlineID ?? -1,
+ });
+
+ Client.AddUserPlaylistItem(userId(), item);
+
+ itemId = item.ID;
+ });
+
+ AddUntilStep("item arrived in playlist", () => playlist.ChildrenOfType>().Any(i => i.Model.ID == itemId));
+ }
+
+ private void deleteItem(int index)
+ {
+ OsuRearrangeableListItem item = null;
+
+ AddStep($"move mouse to delete button {index}", () =>
+ {
+ item = playlist.ChildrenOfType>().ElementAt(index);
+ InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0));
+ });
+
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddUntilStep("item removed from playlist", () => !playlist.ChildrenOfType>().Contains(item));
+ }
+
+ private void assertDeleteButtonVisibility(int index, bool visible)
+ => AddUntilStep($"delete button {index} {(visible ? "is" : "is not")} visible",
+ () => (playlist.ChildrenOfType().ElementAt(index).Alpha > 0) == visible);
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
index 84b63a5733..81220e2527 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
@@ -163,10 +163,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
addClickButtonStep();
+ AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
+
AddStep("transfer host", () => Client.TransferHost(Client.Room?.Users[1].UserID ?? 0));
addClickButtonStep();
- AddAssert("match not started", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
+ AddUntilStep("user is idle (match not started)", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
+ AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value);
}
[TestCase(true)]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs
index 80f807e7d3..4674601f28 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs
@@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using NUnit.Framework;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
-using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer;
+using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -22,20 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
var rulesetInfo = new OsuRuleset().RulesetInfo;
var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
-
- var score = new ScoreInfo
- {
- Rank = ScoreRank.B,
- TotalScore = 987654,
- Accuracy = 0.8,
- MaxCombo = 500,
- Combo = 250,
- BeatmapInfo = beatmapInfo,
- User = new APIUser { Username = "Test user" },
- Date = DateTimeOffset.Now,
- OnlineScoreID = 12345,
- Ruleset = rulesetInfo,
- };
+ var score = TestResources.CreateTestScoreInfo(beatmapInfo);
PlaylistItem playlistItem = new PlaylistItem
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs
index da1fa226e1..f5df8d7507 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs
@@ -1,15 +1,13 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Bindables;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
-using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer;
+using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -26,20 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
var rulesetInfo = new OsuRuleset().RulesetInfo;
var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
-
- var score = new ScoreInfo
- {
- Rank = ScoreRank.B,
- TotalScore = 987654,
- Accuracy = 0.8,
- MaxCombo = 500,
- Combo = 250,
- BeatmapInfo = beatmapInfo,
- User = new APIUser { Username = "Test user" },
- Date = DateTimeOffset.Now,
- OnlineScoreID = 12345,
- Ruleset = rulesetInfo,
- };
+ var score = TestResources.CreateTestScoreInfo(beatmapInfo);
PlaylistItem playlistItem = new PlaylistItem
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs
new file mode 100644
index 0000000000..93ccd5f1e1
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs
@@ -0,0 +1,188 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Drawables;
+using osu.Game.Database;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Rooms;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Screens.OnlinePlay;
+using osu.Game.Screens.OnlinePlay.Playlists;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestScenePlaylistsRoomSettingsPlaylist : OsuManualInputManagerTestScene
+ {
+ private TestPlaylist playlist;
+
+ [Cached(typeof(UserLookupCache))]
+ private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache();
+
+ [Test]
+ public void TestItemRemovedOnDeletion()
+ {
+ PlaylistItem selectedItem = null;
+
+ createPlaylist();
+
+ moveToItem(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ AddStep("retrieve selection", () => selectedItem = playlist.SelectedItem.Value);
+
+ moveToDeleteButton(0);
+ AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("item removed", () => !playlist.Items.Contains(selectedItem));
+ }
+
+ [Test]
+ public void TestNextItemSelectedAfterDeletion()
+ {
+ createPlaylist();
+
+ moveToItem(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ moveToDeleteButton(0);
+ AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
+ }
+
+ [Test]
+ public void TestLastItemSelectedAfterLastItemDeleted()
+ {
+ createPlaylist();
+
+ AddWaitStep("wait for flow", 5); // Items may take 1 update frame to flow. A wait count of 5 is guaranteed to result in the flow being updated as desired.
+ AddStep("scroll to bottom", () => playlist.ChildrenOfType>().First().ScrollToEnd(false));
+
+ moveToItem(19);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ moveToDeleteButton(19);
+ AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("item 18 is selected", () => playlist.SelectedItem.Value == playlist.Items[18]);
+ }
+
+ [Test]
+ public void TestSelectionResetWhenAllItemsDeleted()
+ {
+ createPlaylist();
+
+ AddStep("remove all but one item", () =>
+ {
+ playlist.Items.RemoveRange(1, playlist.Items.Count - 1);
+ });
+
+ moveToItem(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ moveToDeleteButton(0);
+ AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
+ }
+
+ // Todo: currently not possible due to bindable list shortcomings (https://github.com/ppy/osu-framework/issues/3081)
+ // [Test]
+ public void TestNextItemSelectedAfterExternalDeletion()
+ {
+ createPlaylist();
+
+ moveToItem(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ AddStep("remove item 0", () => playlist.Items.RemoveAt(0));
+
+ AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
+ }
+
+ [Test]
+ public void TestChangeBeatmapAndRemove()
+ {
+ createPlaylist();
+
+ AddStep("change beatmap of first item", () => playlist.Items[0].BeatmapID = 30);
+ moveToDeleteButton(0);
+ AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
+ }
+
+ private void moveToItem(int index, Vector2? offset = null)
+ => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset));
+
+ private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () =>
+ {
+ var item = playlist.ChildrenOfType>().ElementAt(index);
+ InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset);
+ });
+
+ private void createPlaylist()
+ {
+ AddStep("create playlist", () =>
+ {
+ Child = playlist = new TestPlaylist
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(500, 300)
+ };
+
+ for (int i = 0; i < 20; i++)
+ {
+ playlist.Items.Add(new PlaylistItem
+ {
+ ID = i,
+ OwnerID = 2,
+ Beatmap =
+ {
+ Value = i % 2 == 1
+ ? new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo
+ : new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Artist = "Artist",
+ Author = new APIUser { Username = "Creator name here" },
+ Title = "Long title used to check background colour",
+ },
+ BeatmapSet = new BeatmapSetInfo()
+ }
+ },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ RequiredMods =
+ {
+ new OsuModHardRock(),
+ new OsuModDoubleTime(),
+ new OsuModAutoplay()
+ }
+ });
+ }
+ });
+
+ AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
+ }
+
+ private class TestPlaylist : PlaylistsRoomSettingsPlaylist
+ {
+ public new IReadOnlyDictionary> ItemMap => base.ItemMap;
+
+ public TestPlaylist()
+ {
+ AllowSelection = true;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
index 35c66e8cda..5aac228f4b 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
@@ -24,9 +24,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestScenePlaylistsSongSelect : OnlinePlayTestScene
{
- [Resolved]
- private BeatmapManager beatmapManager { get; set; }
-
private BeatmapManager manager;
private RulesetStore rulesets;
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
index c70906927e..81c59b90f5 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
@@ -11,6 +11,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@@ -30,9 +31,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
- private TestMultiplayerScreenStack multiplayerScreenStack;
+ private TestMultiplayerComponents multiplayerComponents;
- private TestMultiplayerClient client => multiplayerScreenStack.Client;
+ private TestMultiplayerClient client => multiplayerComponents.Client;
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
@@ -54,8 +55,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
});
- AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack()));
- AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded);
+ AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents()));
+ AddUntilStep("wait for multiplayer to load", () => multiplayerComponents.IsLoaded);
AddUntilStep("wait for lounge to load", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
}
@@ -76,7 +77,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
+ AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
AddAssert("user state arrived", () => client.Room?.Users.FirstOrDefault()?.MatchState is TeamVersusUserState);
}
@@ -102,7 +103,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press own button", () =>
{
- InputManager.MoveMouseTo(multiplayerScreenStack.ChildrenOfType().First());
+ InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType().First());
InputManager.Click(MouseButton.Left);
});
AddAssert("user on team 1", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1);
@@ -112,12 +113,39 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press other user's button", () =>
{
- InputManager.MoveMouseTo(multiplayerScreenStack.ChildrenOfType().ElementAt(1));
+ InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddAssert("user still on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
}
+ [Test]
+ public void TestSettingsUpdatedWhenChangingMatchType()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ Type = { Value = MatchType.HeadToHead },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ AddUntilStep("match type head to head", () => client.APIRoom?.Type.Value == MatchType.HeadToHead);
+
+ AddStep("change match type", () => client.ChangeSettings(new MultiplayerRoomSettings
+ {
+ MatchType = MatchType.TeamVersus
+ }));
+
+ AddUntilStep("api room updated to team versus", () => client.APIRoom?.Type.Value == MatchType.TeamVersus);
+ }
+
[Test]
public void TestChangeTypeViaMatchSettings()
{
@@ -134,31 +162,32 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- AddAssert("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead);
+ AddUntilStep("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead);
- AddUntilStep("team displays are not displaying teams", () => multiplayerScreenStack.ChildrenOfType().All(d => d.DisplayedTeam == null));
+ AddUntilStep("team displays are not displaying teams", () => multiplayerComponents.ChildrenOfType().All(d => d.DisplayedTeam == null));
AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus));
- AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
+ AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
- AddUntilStep("team displays are displaying teams", () => multiplayerScreenStack.ChildrenOfType().All(d => d.DisplayedTeam != null));
+ AddUntilStep("team displays are displaying teams", () => multiplayerComponents.ChildrenOfType().All(d => d.DisplayedTeam != null));
}
private void createRoom(Func room)
{
- AddStep("open room", () => multiplayerScreenStack.ChildrenOfType().Single().Open(room()));
+ AddStep("open room", () => multiplayerComponents.ChildrenOfType().Single().Open(room()));
AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2);
+ AddUntilStep("create room button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
AddStep("create room", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType().Single());
InputManager.Click(MouseButton.Left);
});
- AddUntilStep("wait for join", () => multiplayerScreenStack.RoomManager.RoomJoined);
+ AddUntilStep("wait for join", () => client.RoomJoined);
}
}
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs
new file mode 100644
index 0000000000..06306ad197
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . 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.Allocation;
+using osu.Framework.Testing;
+using osu.Game.Overlays.Settings.Sections;
+using osu.Game.Skinning;
+using osu.Game.Skinning.Editor;
+
+namespace osu.Game.Tests.Visual.Navigation
+{
+ public class TestSceneEditDefaultSkin : OsuGameTestScene
+ {
+ private SkinManager skinManager => Game.Dependencies.Get();
+ private SkinEditorOverlay skinEditor => Game.Dependencies.Get();
+
+ [Test]
+ public void TestEditDefaultSkin()
+ {
+ AddAssert("is default skin", () => skinManager.CurrentSkinInfo.Value.ID == SkinInfo.DEFAULT_SKIN);
+
+ AddStep("open settings", () => { Game.Settings.Show(); });
+
+ // Until step requires as settings has a delayed load.
+ AddUntilStep("export button disabled", () => Game.Settings.ChildrenOfType().SingleOrDefault()?.Enabled.Value == false);
+
+ // Will create a mutable skin.
+ AddStep("open skin editor", () => skinEditor.Show());
+
+ // Until step required as the skin editor may take time to load (and an extra scheduled frame for the mutable part).
+ AddUntilStep("is modified default skin", () => skinManager.CurrentSkinInfo.Value.ID != SkinInfo.DEFAULT_SKIN);
+ AddAssert("is not protected", () => skinManager.CurrentSkinInfo.Value.PerformRead(s => !s.Protected));
+
+ AddUntilStep("export button enabled", () => Game.Settings.ChildrenOfType().SingleOrDefault()?.Enabled.Value == true);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs
new file mode 100644
index 0000000000..9e684e4f10
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs
@@ -0,0 +1,99 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Configuration;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Beatmaps.IO;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Navigation
+{
+ public class TestSceneMouseWheelVolumeAdjust : OsuGameTestScene
+ {
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ // Headless tests are always at minimum volume. This covers interactive tests, matching that initial value.
+ AddStep("Set volume to min", () => Game.Audio.Volume.Value = 0);
+ AddAssert("Volume is min", () => Game.Audio.AggregateVolume.Value == 0);
+ AddStep("Move mouse to centre", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
+ }
+
+ [Test]
+ public void TestAdjustVolumeFromMainMenu()
+ {
+ // First scroll makes volume controls appear, second adjusts volume.
+ AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2);
+ AddUntilStep("Volume is above zero", () => Game.Audio.AggregateVolume.Value > 0);
+ }
+
+ [Test]
+ public void TestAdjustVolumeFromPlayerWheelEnabled()
+ {
+ loadToPlayerNonBreakTime();
+
+ // First scroll makes volume controls appear, second adjusts volume.
+ AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2);
+ AddAssert("Volume is above zero", () => Game.Audio.Volume.Value > 0);
+ }
+
+ [Test]
+ public void TestAdjustVolumeFromPlayerWheelDisabled()
+ {
+ AddStep("disable wheel volume adjust", () => Game.LocalConfig.SetValue(OsuSetting.MouseDisableWheel, true));
+
+ loadToPlayerNonBreakTime();
+
+ // First scroll makes volume controls appear, second adjusts volume.
+ AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2);
+ AddAssert("Volume is still zero", () => Game.Audio.Volume.Value == 0);
+ }
+
+ [Test]
+ public void TestAdjustVolumeFromPlayerWheelDisabledHoldingAlt()
+ {
+ AddStep("disable wheel volume adjust", () => Game.LocalConfig.SetValue(OsuSetting.MouseDisableWheel, true));
+
+ loadToPlayerNonBreakTime();
+
+ // First scroll makes volume controls appear, second adjusts volume.
+ AddRepeatStep("Adjust volume using mouse wheel holding alt", () =>
+ {
+ InputManager.PressKey(Key.AltLeft);
+ InputManager.ScrollVerticalBy(5);
+ InputManager.ReleaseKey(Key.AltLeft);
+ }, 2);
+
+ AddAssert("Volume is above zero", () => Game.Audio.Volume.Value > 0);
+ }
+
+ private void loadToPlayerNonBreakTime()
+ {
+ Player player = null;
+ Screens.Select.SongSelect songSelect = null;
+ PushAndConfirm(() => songSelect = new TestSceneScreenNavigation.TestPlaySongSelect());
+ AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
+
+ AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait());
+ AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
+ AddStep("press enter", () => InputManager.Key(Key.Enter));
+
+ AddUntilStep("wait for player", () =>
+ {
+ // dismiss any notifications that may appear (ie. muted notification).
+ clickMouseInCentre();
+ return (player = Game.ScreenStack.CurrentScreen as Player) != null;
+ });
+
+ AddUntilStep("wait for play time active", () => !player.IsBreakTime.Value);
+ }
+
+ private void clickMouseInCentre()
+ {
+ InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre);
+ InputManager.Click(MouseButton.Left);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
index 2706ff5ceb..4d1e279090 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
@@ -83,9 +83,6 @@ namespace osu.Game.Tests.Visual.Navigation
[Resolved]
private OsuGameBase gameBase { get; set; }
- [Resolved]
- private GameHost host { get; set; }
-
[Test]
public void TestNullRulesetHandled()
{
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
index c9dec25ad3..1653247570 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
@@ -128,7 +128,7 @@ namespace osu.Game.Tests.Visual.Navigation
imported = Game.ScoreManager.Import(new ScoreInfo
{
Hash = Guid.NewGuid().ToString(),
- OnlineScoreID = i,
+ OnlineID = i,
BeatmapInfo = beatmap.Beatmaps.First(),
Ruleset = ruleset ?? new OsuRuleset().RulesetInfo
}).Result.Value;
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index 664c186cf8..48ab643992 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -336,12 +336,12 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestPushMatchSubScreenAndPressBackButtonImmediately()
{
- TestMultiplayerScreenStack multiplayerScreenStack = null;
+ TestMultiplayerComponents multiplayerComponents = null;
- PushAndConfirm(() => multiplayerScreenStack = new TestMultiplayerScreenStack());
+ PushAndConfirm(() => multiplayerComponents = new TestMultiplayerComponents());
- AddUntilStep("wait for lounge", () => multiplayerScreenStack.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
- AddStep("open room", () => multiplayerScreenStack.ChildrenOfType().Single().Open());
+ AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
+ AddStep("open room", () => multiplayerComponents.ChildrenOfType().Single().Open());
AddStep("press back button", () => Game.ChildrenOfType().First().Action());
AddWaitStep("wait two frames", 2);
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs
index 90f3eb64e4..63741451f3 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Online
}
[Resolved]
- private RulesetStore rulesets { get; set; }
+ private IRulesetStore rulesets { get; set; }
[Test]
public void TestMultipleRulesetsBeatmapSet()
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index 3314e291e8..f87cca80b0 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Online
}
[Resolved]
- private RulesetStore rulesets { get; set; }
+ private IRulesetStore rulesets { get; set; }
[Test]
public void TestLoading()
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
index 7028ecf39f..14f32df653 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
@@ -13,7 +13,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
-using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
@@ -47,9 +46,6 @@ namespace osu.Game.Tests.Visual.Online
[CanBeNull]
private Func> onGetMessages;
- [Resolved]
- private GameHost host { get; set; }
-
public TestSceneChatOverlay()
{
channels = Enumerable.Range(1, 10)
@@ -397,6 +393,25 @@ namespace osu.Game.Tests.Visual.Online
channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single().Username == "some body");
}
+ [Test]
+ public void TestMultiplayerChannelIsNotShown()
+ {
+ Channel multiplayerChannel = null;
+
+ AddStep("join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser())
+ {
+ Name = "#mp_1",
+ Type = ChannelType.Multiplayer,
+ }));
+
+ AddAssert("channel joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel));
+ AddAssert("channel not present in overlay", () => !chatOverlay.TabMap.ContainsKey(multiplayerChannel));
+ AddAssert("multiplayer channel is not current", () => channelManager.CurrentChannel.Value != multiplayerChannel);
+
+ AddStep("leave channel", () => channelManager.LeaveChannel(multiplayerChannel));
+ AddAssert("channel left", () => !channelManager.JoinedChannels.Contains(multiplayerChannel));
+ }
+
private void pressChannelHotkey(int number)
{
var channelKey = Key.Number0 + number;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
index 50969aad9b..be2db9a8a0 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
@@ -47,9 +47,9 @@ namespace osu.Game.Tests.Visual.Online
var allScores = new APIScoresCollection
{
- Scores = new List
+ Scores = new List
{
- new APIScoreInfo
+ new APIScore
{
User = new APIUser
{
@@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567890,
Accuracy = 1,
},
- new APIScoreInfo
+ new APIScore
{
User = new APIUser
{
@@ -98,7 +98,7 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234789,
Accuracy = 0.9997,
},
- new APIScoreInfo
+ new APIScore
{
User = new APIUser
{
@@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 12345678,
Accuracy = 0.9854,
},
- new APIScoreInfo
+ new APIScore
{
User = new APIUser
{
@@ -143,7 +143,7 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567,
Accuracy = 0.8765,
},
- new APIScoreInfo
+ new APIScore
{
User = new APIUser
{
@@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.Online
var myBestScore = new APIScoreWithPosition
{
- Score = new APIScoreInfo
+ Score = new APIScore
{
User = new APIUser
{
@@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.Online
var myBestScoreWithNullPosition = new APIScoreWithPosition
{
- Score = new APIScoreInfo
+ Score = new APIScore
{
User = new APIUser
{
@@ -212,9 +212,9 @@ namespace osu.Game.Tests.Visual.Online
var oneScore = new APIScoresCollection
{
- Scores = new List
+ Scores = new List
{
- new APIScoreInfo
+ new APIScore
{
User = new APIUser
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
index 19e06beaad..52d5eb2c65 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Online
private TestUserListPanel evast;
[Resolved]
- private RulesetStore rulesetStore { get; set; }
+ private IRulesetStore rulesetStore { get; set; }
[SetUp]
public void SetUp() => Schedule(() =>
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs
index 76997bded7..dda9543159 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs
@@ -2,9 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
+using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
+using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile;
@@ -14,72 +15,77 @@ namespace osu.Game.Tests.Visual.Online
{
public class TestSceneUserProfileHeader : OsuTestScene
{
- protected override bool UseOnlineAPI => true;
-
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
- [Resolved]
- private IAPIProvider api { get; set; }
+ private ProfileHeader header;
- private readonly ProfileHeader header;
-
- public TestSceneUserProfileHeader()
+ [SetUpSteps]
+ public void SetUpSteps()
{
- header = new ProfileHeader();
- Add(header);
+ AddStep("create header", () => Child = header = new ProfileHeader());
+ }
- AddStep("Show test dummy", () => header.User.Value = TestSceneUserProfileOverlay.TEST_USER);
+ [Test]
+ public void TestBasic()
+ {
+ AddStep("Show example user", () => header.User.Value = TestSceneUserProfileOverlay.TEST_USER);
+ }
- AddStep("Show null dummy", () => header.User.Value = new APIUser
- {
- Username = "Null"
- });
-
- AddStep("Show online dummy", () => header.User.Value = new APIUser
+ [Test]
+ public void TestOnlineState()
+ {
+ AddStep("Show online user", () => header.User.Value = new APIUser
{
+ Id = 1001,
Username = "IAmOnline",
LastVisit = DateTimeOffset.Now,
IsOnline = true,
});
- AddStep("Show offline dummy", () => header.User.Value = new APIUser
+ AddStep("Show offline user", () => header.User.Value = new APIUser
{
+ Id = 1002,
Username = "IAmOffline",
- LastVisit = DateTimeOffset.Now,
+ LastVisit = DateTimeOffset.Now.AddDays(-10),
IsOnline = false,
});
-
- addOnlineStep("Show ppy", new APIUser
- {
- Username = @"peppy",
- Id = 2,
- IsSupporter = true,
- Country = new Country { FullName = @"Australia", FlagName = @"AU" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg"
- });
-
- addOnlineStep("Show flyte", new APIUser
- {
- Username = @"flyte",
- Id = 3103765,
- Country = new Country { FullName = @"Japan", FlagName = @"JP" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
- });
}
- private void addOnlineStep(string name, APIUser fallback)
+ [Test]
+ public void TestRankedState()
{
- AddStep(name, () =>
+ AddStep("Show ranked user", () => header.User.Value = new APIUser
{
- if (api.IsLoggedIn)
+ Id = 2001,
+ Username = "RankedUser",
+ Statistics = new UserStatistics
{
- var request = new GetUserRequest(fallback.Id);
- request.Success += user => header.User.Value = user;
- api.Queue(request);
+ IsRanked = true,
+ GlobalRank = 15000,
+ CountryRank = 1500,
+ RankHistory = new APIRankHistory
+ {
+ Mode = @"osu",
+ Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray()
+ },
+ }
+ });
+
+ AddStep("Show unranked user", () => header.User.Value = new APIUser
+ {
+ Id = 2002,
+ Username = "UnrankedUser",
+ Statistics = new UserStatistics
+ {
+ IsRanked = false,
+ // web will sometimes return non-empty rank history even for unranked users.
+ RankHistory = new APIRankHistory
+ {
+ Mode = @"osu",
+ Data = Enumerable.Range(2345, 85).ToArray()
+ },
}
- else
- header.User.Value = fallback;
});
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
index ce8136199f..78e2ceb45b 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
@@ -4,8 +4,6 @@
using System;
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Allocation;
-using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile;
@@ -20,9 +18,6 @@ namespace osu.Game.Tests.Visual.Online
private readonly TestUserProfileOverlay profile;
- [Resolved]
- private IAPIProvider api { get; set; }
-
public static readonly APIUser TEST_USER = new APIUser
{
Username = @"Somebody",
@@ -34,6 +29,7 @@ namespace osu.Game.Tests.Visual.Online
ProfileOrder = new[] { "me" },
Statistics = new UserStatistics
{
+ IsRanked = true,
GlobalRank = 2148,
CountryRank = 1,
PP = 4567.89m,
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs
index 9c2cc13416..7dfdca8276 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Online
{
public TestSceneUserProfileScores()
{
- var firstScore = new APIScoreInfo
+ var firstScore = new APIScore
{
PP = 1047.21,
Rank = ScoreRank.SH,
@@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online
Accuracy = 0.9813
};
- var secondScore = new APIScoreInfo
+ var secondScore = new APIScore
{
PP = 134.32,
Rank = ScoreRank.A,
@@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online
Accuracy = 0.998546
};
- var thirdScore = new APIScoreInfo
+ var thirdScore = new APIScore
{
PP = 96.83,
Rank = ScoreRank.S,
@@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Online
Accuracy = 0.9726
};
- var noPPScore = new APIScoreInfo
+ var noPPScore = new APIScore
{
Rank = ScoreRank.B,
Beatmap = new APIBeatmap
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index 4284bc6358..e9210496ca 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -22,14 +22,17 @@ using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Playlists
{
public class TestScenePlaylistsResultsScreen : ScreenTestScene
{
private const int scores_per_result = 10;
+ private const int real_user_position = 200;
private TestResultsScreen resultsScreen;
+
private int currentScoreId;
private bool requestComplete;
private int totalCount;
@@ -37,7 +40,7 @@ namespace osu.Game.Tests.Visual.Playlists
[SetUp]
public void Setup() => Schedule(() =>
{
- currentScoreId = 0;
+ currentScoreId = 1;
requestComplete = false;
totalCount = 0;
bindHandler();
@@ -50,13 +53,17 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("bind user score info handler", () =>
{
- userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
+ userScore = TestResources.CreateTestScoreInfo();
+ userScore.OnlineID = currentScoreId++;
+
bindHandler(userScore: userScore);
});
createResults(() => userScore);
- AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
+ AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
+ AddAssert($"score panel position is {real_user_position}",
+ () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).ScorePosition.Value == real_user_position);
}
[Test]
@@ -74,14 +81,16 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("bind user score info handler", () =>
{
- userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
+ userScore = TestResources.CreateTestScoreInfo();
+ userScore.OnlineID = currentScoreId++;
+
bindHandler(true, userScore);
});
createResults(() => userScore);
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1);
- AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
+ AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
}
[Test]
@@ -123,7 +132,9 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("bind user score info handler", () =>
{
- userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
+ userScore = TestResources.CreateTestScoreInfo();
+ userScore.OnlineID = currentScoreId++;
+
bindHandler(userScore: userScore);
});
@@ -157,12 +168,13 @@ namespace osu.Game.Tests.Visual.Playlists
}));
});
+ AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
waitForDisplay();
}
private void waitForDisplay()
{
- AddUntilStep("wait for load to complete", () =>
+ AddUntilStep("wait for scores loaded", () =>
requestComplete
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
&& resultsScreen.ScorePanelList.AllPanelsVisible);
@@ -230,12 +242,12 @@ namespace osu.Game.Tests.Visual.Playlists
{
var multiplayerUserScore = new MultiplayerScore
{
- ID = (int)(userScore.OnlineScoreID ?? currentScoreId++),
+ ID = (int)(userScore.OnlineID > 0 ? userScore.OnlineID : currentScoreId++),
Accuracy = userScore.Accuracy,
EndedAt = userScore.Date,
Passed = userScore.Passed,
Rank = userScore.Rank,
- Position = 200,
+ Position = real_user_position,
MaxCombo = userScore.MaxCombo,
TotalScore = userScore.TotalScore,
User = userScore.User,
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
index c5287d4257..a426f075e1 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
@@ -15,9 +15,11 @@ using osu.Game.Database;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play;
+using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Input;
@@ -112,37 +114,80 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestBeatmapUpdatedOnReImport()
{
- BeatmapSetInfo importedSet = null;
+ string realHash = null;
+ int realOnlineId = 0;
+ int realOnlineSetId = 0;
- AddStep("import altered beatmap", () =>
+ AddStep("store real beatmap values", () =>
{
- IBeatmap beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
-
- beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1;
-
- // intentionally increment online IDs to clash with import below.
- beatmap.BeatmapInfo.OnlineID++;
- beatmap.BeatmapInfo.BeatmapSet.OnlineID++;
-
- importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result.Value;
+ realHash = importedBeatmap.Value.Beatmaps[0].MD5Hash;
+ realOnlineId = importedBeatmap.Value.Beatmaps[0].OnlineID ?? -1;
+ realOnlineSetId = importedBeatmap.Value.OnlineID ?? -1;
});
+ AddStep("import modified beatmap", () =>
+ {
+ var modifiedBeatmap = new TestBeatmap(new OsuRuleset().RulesetInfo)
+ {
+ BeatmapInfo =
+ {
+ OnlineID = realOnlineId,
+ BeatmapSet =
+ {
+ OnlineID = realOnlineSetId
+ }
+ },
+ };
+
+ modifiedBeatmap.HitObjects.Clear();
+ modifiedBeatmap.HitObjects.Add(new HitCircle { StartTime = 5000 });
+
+ manager.Import(modifiedBeatmap.BeatmapInfo.BeatmapSet).Wait();
+ });
+
+ // Create the room using the real beatmap values.
setupAndCreateRoom(room =>
{
room.Name.Value = "my awesome room";
room.Host.Value = API.LocalUser.Value;
room.Playlist.Add(new PlaylistItem
{
- Beatmap = { Value = importedSet.Beatmaps[0] },
+ Beatmap =
+ {
+ Value = new BeatmapInfo
+ {
+ MD5Hash = realHash,
+ OnlineID = realOnlineId,
+ BeatmapSet = new BeatmapSetInfo
+ {
+ OnlineID = realOnlineSetId,
+ }
+ }
+ },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
});
});
- AddAssert("match has altered beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize == 1);
+ AddAssert("match has default beatmap", () => match.Beatmap.IsDefault);
- importBeatmap();
+ AddStep("reimport original beatmap", () =>
+ {
+ var originalBeatmap = new TestBeatmap(new OsuRuleset().RulesetInfo)
+ {
+ BeatmapInfo =
+ {
+ OnlineID = realOnlineId,
+ BeatmapSet =
+ {
+ OnlineID = realOnlineSetId
+ }
+ },
+ };
- AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize != 1);
+ manager.Import(originalBeatmap.BeatmapInfo.BeatmapSet).Wait();
+ });
+
+ AddUntilStep("match has correct beatmap", () => realHash == match.Beatmap.Value.BeatmapInfo.MD5Hash);
}
private void setupAndCreateRoom(Action room)
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs
index acacdf8644..85306b9354 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . 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.Allocation;
using osu.Framework.Bindables;
@@ -9,30 +10,32 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
-using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Contracted;
+using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
{
public class TestSceneContractedPanelMiddleContent : OsuTestScene
{
- [Resolved]
- private RulesetStore rulesetStore { get; set; }
-
[Test]
public void TestShowPanel()
{
- AddStep("show example score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new TestScoreInfo(new OsuRuleset().RulesetInfo)));
+ AddStep("show example score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), TestResources.CreateTestScoreInfo()));
}
[Test]
public void TestExcessMods()
{
- AddStep("show excess mods score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new TestScoreInfo(new OsuRuleset().RulesetInfo, true)));
+ AddStep("show excess mods score", () =>
+ {
+ var score = TestResources.CreateTestScoreInfo();
+ score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray();
+ showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), score);
+ });
}
private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score)
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
index 9983993d9c..2cb4fb6b6b 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
@@ -20,6 +20,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Expanded;
using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
@@ -34,21 +35,21 @@ namespace osu.Game.Tests.Visual.Ranking
{
var author = new APIUser { Username = "mapper_name" };
- AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo)
- {
- BeatmapInfo = createTestBeatmap(author)
- }));
+ AddStep("show example score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(author))));
}
[Test]
public void TestExcessMods()
{
- var author = new APIUser { Username = "mapper_name" };
-
- AddStep("show excess mods score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo, true)
+ AddStep("show excess mods score", () =>
{
- BeatmapInfo = createTestBeatmap(author)
- }));
+ var author = new APIUser { Username = "mapper_name" };
+
+ var score = TestResources.CreateTestScoreInfo(createTestBeatmap(author));
+ score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray();
+
+ showPanel(score);
+ });
AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Current.Value == "mapper_name"));
}
@@ -56,10 +57,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestMapWithUnknownMapper()
{
- AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo)
- {
- BeatmapInfo = createTestBeatmap(new APIUser())
- }));
+ AddStep("show example score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(new APIUser()))));
AddAssert("mapped by text not present", () =>
this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text.ToString(), "mapped", "by")));
@@ -77,12 +75,12 @@ namespace osu.Game.Tests.Visual.Ranking
var mods = new Mod[] { ruleset.GetAutoplayMod() };
var beatmap = createTestBeatmap(new APIUser());
- showPanel(new TestScoreInfo(ruleset.RulesetInfo)
- {
- Mods = mods,
- BeatmapInfo = beatmap,
- Date = default,
- });
+ var score = TestResources.CreateTestScoreInfo(beatmap);
+
+ score.Mods = mods;
+ score.Date = default;
+
+ showPanel(score);
});
AddAssert("play time not displayed", () => !this.ChildrenOfType().Any());
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs
index a32bcbe7f0..a2fa142896 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs
@@ -5,8 +5,8 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Ranking.Expanded;
+using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
@@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Ranking
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#444"),
},
- new ExpandedPanelTopContent(new TestScoreInfo(new OsuRuleset().RulesetInfo).User),
+ new ExpandedPanelTopContent(TestResources.CreateTestScoreInfo().User),
}
};
}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index 94700bac6a..d0bd5a6e66 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -15,12 +15,12 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
-using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Statistics;
+using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
@@ -72,11 +72,10 @@ namespace osu.Game.Tests.Visual.Ranking
{
TestResultsScreen screen = null;
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo)
- {
- Accuracy = accuracy,
- Rank = rank
- };
+ var score = TestResources.CreateTestScoreInfo();
+
+ score.Accuracy = accuracy;
+ score.Rank = rank;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen(score)));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
@@ -204,7 +203,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
DelayedFetchResultsScreen screen = null;
- AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo), 3000)));
+ AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(TestResources.CreateTestScoreInfo(), 3000)));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddStep("click expanded panel", () =>
{
@@ -237,9 +236,9 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("download button is enabled", () => screen.ChildrenOfType().Last().Enabled.Value);
}
- private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? new TestScoreInfo(new OsuRuleset().RulesetInfo));
+ private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? TestResources.CreateTestScoreInfo());
- private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo));
+ private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(TestResources.CreateTestScoreInfo());
private class TestResultsContainer : Container
{
@@ -282,7 +281,7 @@ namespace osu.Game.Tests.Visual.Ranking
for (int i = 0; i < 20; i++)
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+ var score = TestResources.CreateTestScoreInfo();
score.TotalScore += 10 - i;
score.Hash = $"test{i}";
scores.Add(score);
@@ -316,7 +315,7 @@ namespace osu.Game.Tests.Visual.Ranking
for (int i = 0; i < 20; i++)
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+ var score = TestResources.CreateTestScoreInfo();
score.TotalScore += 10 - i;
scores.Add(score);
}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs
index 5af55e99f8..5dbeefd390 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs
@@ -3,10 +3,10 @@
using NUnit.Framework;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
+using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Ranking
{
@@ -17,7 +17,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestDRank()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.5, Rank = ScoreRank.D };
+ var score = TestResources.CreateTestScoreInfo();
+ score.Accuracy = 0.5;
+ score.Rank = ScoreRank.D;
addPanelStep(score);
}
@@ -25,7 +27,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestCRank()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.75, Rank = ScoreRank.C };
+ var score = TestResources.CreateTestScoreInfo();
+ score.Accuracy = 0.75;
+ score.Rank = ScoreRank.C;
addPanelStep(score);
}
@@ -33,7 +37,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestBRank()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.85, Rank = ScoreRank.B };
+ var score = TestResources.CreateTestScoreInfo();
+ score.Accuracy = 0.85;
+ score.Rank = ScoreRank.B;
addPanelStep(score);
}
@@ -41,7 +47,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestARank()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A };
+ var score = TestResources.CreateTestScoreInfo();
+ score.Accuracy = 0.925;
+ score.Rank = ScoreRank.A;
addPanelStep(score);
}
@@ -49,7 +57,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestSRank()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.975, Rank = ScoreRank.S };
+ var score = TestResources.CreateTestScoreInfo();
+ score.Accuracy = 0.975;
+ score.Rank = ScoreRank.S;
addPanelStep(score);
}
@@ -57,7 +67,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAlmostSSRank()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.9999, Rank = ScoreRank.S };
+ var score = TestResources.CreateTestScoreInfo();
+ score.Accuracy = 0.9999;
+ score.Rank = ScoreRank.S;
addPanelStep(score);
}
@@ -65,7 +77,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestSSRank()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 1, Rank = ScoreRank.X };
+ var score = TestResources.CreateTestScoreInfo();
+ score.Accuracy = 1;
+ score.Rank = ScoreRank.X;
addPanelStep(score);
}
@@ -73,7 +87,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAllHitResults()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Statistics = { [HitResult.Perfect] = 350, [HitResult.Ok] = 200 } };
+ var score = TestResources.CreateTestScoreInfo();
+ score.Statistics[HitResult.Perfect] = 350;
+ score.Statistics[HitResult.Ok] = 200;
addPanelStep(score);
}
@@ -81,7 +97,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestContractedPanel()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A };
+ var score = TestResources.CreateTestScoreInfo();
+ score.Accuracy = 0.925;
+ score.Rank = ScoreRank.A;
addPanelStep(score, PanelState.Contracted);
}
@@ -89,7 +107,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestExpandAndContract()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A };
+ var score = TestResources.CreateTestScoreInfo();
+ score.Accuracy = 0.925;
+ score.Rank = ScoreRank.A;
addPanelStep(score, PanelState.Contracted);
AddWaitStep("wait for transition", 10);
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs
index b7b7407428..f5ad352b9c 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs
@@ -7,9 +7,9 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
-using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
+using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Ranking
@@ -29,14 +29,14 @@ namespace osu.Game.Tests.Visual.Ranking
{
createListStep(() => new ScorePanelList
{
- SelectedScore = { Value = new TestScoreInfo(new OsuRuleset().RulesetInfo) }
+ SelectedScore = { Value = TestResources.CreateTestScoreInfo() }
});
}
[Test]
public void TestAddPanelAfterSelectingScore()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+ var score = TestResources.CreateTestScoreInfo();
createListStep(() => new ScorePanelList
{
@@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAddPanelBeforeSelectingScore()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+ var score = TestResources.CreateTestScoreInfo();
createListStep(() => new ScorePanelList());
@@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Ranking
AddStep("add many scores", () =>
{
for (int i = 0; i < 20; i++)
- list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo));
+ list.AddScore(TestResources.CreateTestScoreInfo());
});
assertFirstPanelCentred();
@@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAddManyScoresAfterExpandedPanel()
{
- var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+ var initialScore = TestResources.CreateTestScoreInfo();
createListStep(() => new ScorePanelList());
@@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Ranking
AddStep("add many scores", () =>
{
for (int i = 0; i < 20; i++)
- list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 });
+ list.AddScore(createScoreForTotalScore(initialScore.TotalScore - i - 1));
});
assertScoreState(initialScore, true);
@@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAddManyScoresBeforeExpandedPanel()
{
- var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+ var initialScore = TestResources.CreateTestScoreInfo();
createListStep(() => new ScorePanelList());
@@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Ranking
AddStep("add scores", () =>
{
for (int i = 0; i < 20; i++)
- list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 });
+ list.AddScore(createScoreForTotalScore(initialScore.TotalScore + i + 1));
});
assertScoreState(initialScore, true);
@@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAddManyPanelsOnBothSidesOfExpandedPanel()
{
- var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+ var initialScore = TestResources.CreateTestScoreInfo();
createListStep(() => new ScorePanelList());
@@ -143,10 +143,10 @@ namespace osu.Game.Tests.Visual.Ranking
AddStep("add scores after", () =>
{
for (int i = 0; i < 20; i++)
- list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 });
+ list.AddScore(createScoreForTotalScore(initialScore.TotalScore - i - 1));
for (int i = 0; i < 20; i++)
- list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 });
+ list.AddScore(createScoreForTotalScore(initialScore.TotalScore + i + 1));
});
assertScoreState(initialScore, true);
@@ -156,11 +156,11 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestSelectMultipleScores()
{
- var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
- var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+ var firstScore = TestResources.CreateTestScoreInfo();
+ var secondScore = TestResources.CreateTestScoreInfo();
- firstScore.User.Username = "A";
- secondScore.User.Username = "B";
+ firstScore.UserString = "A";
+ secondScore.UserString = "B";
createListStep(() => new ScorePanelList());
@@ -190,7 +190,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAddScoreImmediately()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+ var score = TestResources.CreateTestScoreInfo();
createListStep(() =>
{
@@ -206,9 +206,14 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestKeyboardNavigation()
{
- var lowestScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 100 };
- var middleScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 200 };
- var highestScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 300 };
+ var lowestScore = TestResources.CreateTestScoreInfo();
+ lowestScore.MaxCombo = 100;
+
+ var middleScore = TestResources.CreateTestScoreInfo();
+ middleScore.MaxCombo = 200;
+
+ var highestScore = TestResources.CreateTestScoreInfo();
+ highestScore.MaxCombo = 300;
createListStep(() => new ScorePanelList());
@@ -270,6 +275,13 @@ namespace osu.Game.Tests.Visual.Ranking
assertExpandedPanelCentred();
}
+ private ScoreInfo createScoreForTotalScore(long totalScore)
+ {
+ var score = TestResources.CreateTestScoreInfo();
+ score.TotalScore = totalScore;
+ return score;
+ }
+
private void createListStep(Func creationFunc)
{
AddStep("create list", () => Child = list = creationFunc().With(d =>
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
index d91aec753c..f64b7b2b65 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
@@ -6,11 +6,11 @@ using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
@@ -20,10 +20,8 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestScoreWithTimeStatistics()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo)
- {
- HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents()
- };
+ var score = TestResources.CreateTestScoreInfo();
+ score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents();
loadPanel(score);
}
@@ -31,10 +29,8 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestScoreWithPositionStatistics()
{
- var score = new TestScoreInfo(new OsuRuleset().RulesetInfo)
- {
- HitEvents = createPositionDistributedHitEvents()
- };
+ var score = TestResources.CreateTestScoreInfo();
+ score.HitEvents = createPositionDistributedHitEvents();
loadPanel(score);
}
@@ -42,7 +38,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestScoreWithoutStatistics()
{
- loadPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo));
+ loadPanel(TestResources.CreateTestScoreInfo());
}
[Test]
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
index e7c54efa8c..9ad5242df4 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
@@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.SongSelect
beatmaps.Add(testBeatmap);
- AddStep("set ruleset", () => Ruleset.Value = rulesetInfo);
+ setRuleset(rulesetInfo);
selectBeatmap(testBeatmap);
@@ -167,6 +167,22 @@ namespace osu.Game.Tests.Visual.SongSelect
label => label.Statistic.Name == "BPM" && label.Statistic.Content == target.ToString(CultureInfo.InvariantCulture)));
}
+ private void setRuleset(RulesetInfo rulesetInfo)
+ {
+ Container containerBefore = null;
+
+ AddStep("set ruleset", () =>
+ {
+ // wedge content is only refreshed if the ruleset changes, so only wait for load in that case.
+ if (!rulesetInfo.Equals(Ruleset.Value))
+ containerBefore = infoWedge.DisplayedContent;
+
+ Ruleset.Value = rulesetInfo;
+ });
+
+ AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
+ }
+
private void selectBeatmap([CanBeNull] IBeatmap b)
{
Container containerBefore = null;
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 0494d1de3c..912d3f838c 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -466,7 +466,9 @@ namespace osu.Game.Tests.Visual.SongSelect
public void TestExternalBeatmapChangeWhileFiltered(bool differentRuleset)
{
createSongSelect();
- addManyTestMaps();
+ // ensure there is at least 1 difficulty for each of the rulesets
+ // (catch is excluded inside of addManyTestMaps).
+ addManyTestMaps(3);
changeRuleset(0);
@@ -488,8 +490,9 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select beatmap externally", () =>
{
target = manager.GetAllUsableBeatmapSets()
- .Where(b => b.Beatmaps.Any(bi => bi.RulesetID == targetRuleset))
- .ElementAt(5).Beatmaps.First(bi => bi.RulesetID == targetRuleset);
+ .First(b => b.Beatmaps.Any(bi => bi.RulesetID == targetRuleset))
+ .Beatmaps
+ .First(bi => bi.RulesetID == targetRuleset);
Beatmap.Value = manager.GetWorkingBeatmap(target);
});
@@ -518,7 +521,9 @@ namespace osu.Game.Tests.Visual.SongSelect
public void TestExternalBeatmapChangeWhileFilteredThenRefilter()
{
createSongSelect();
- addManyTestMaps();
+ // ensure there is at least 1 difficulty for each of the rulesets
+ // (catch is excluded inside of addManyTestMaps).
+ addManyTestMaps(3);
changeRuleset(0);
@@ -534,8 +539,10 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select beatmap externally", () =>
{
- target = manager.GetAllUsableBeatmapSets().Where(b => b.Beatmaps.Any(bi => bi.RulesetID == 1))
- .ElementAt(5).Beatmaps.First();
+ target = manager
+ .GetAllUsableBeatmapSets()
+ .First(b => b.Beatmaps.Any(bi => bi.RulesetID == 1))
+ .Beatmaps.First();
Beatmap.Value = manager.GetWorkingBeatmap(target);
});
@@ -835,12 +842,7 @@ namespace osu.Game.Tests.Visual.SongSelect
// this beatmap change should be overridden by the present.
Beatmap.Value = manager.GetWorkingBeatmap(getSwitchBeatmap());
- songSelect.PresentScore(new ScoreInfo
- {
- User = new APIUser { Username = "woo" },
- BeatmapInfo = getPresentBeatmap(),
- Ruleset = getPresentBeatmap().Ruleset
- });
+ songSelect.PresentScore(TestResources.CreateTestScoreInfo(getPresentBeatmap()));
});
AddUntilStep("wait for results screen presented", () => !songSelect.IsCurrentScreen());
@@ -882,14 +884,21 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for carousel loaded", () => songSelect.Carousel.IsAlive);
}
- private void addManyTestMaps()
+ ///
+ /// Imports test beatmap sets to show in the carousel.
+ ///
+ ///
+ /// The exact count of difficulties to create for each beatmap set.
+ /// A value causes the count of difficulties to be selected randomly.
+ ///
+ private void addManyTestMaps(int? difficultyCountPerSet = null)
{
AddStep("import test maps", () =>
{
var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray();
- for (int i = 0; i < 100; i += 10)
- manager.Import(TestResources.CreateTestBeatmapSetInfo(rulesets: usableRulesets)).Wait();
+ for (int i = 0; i < 10; i++)
+ manager.Import(TestResources.CreateTestBeatmapSetInfo(difficultyCountPerSet, usableRulesets)).Wait();
});
}
diff --git a/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs
similarity index 88%
rename from osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs
rename to osu.Game.Tests/Visual/TestMultiplayerComponents.cs
index 7f1171db1f..cd7a936778 100644
--- a/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs
+++ b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual
///
///
///
- public class TestMultiplayerScreenStack : OsuScreen
+ public class TestMultiplayerComponents : OsuScreen
{
public Screens.OnlinePlay.Multiplayer.Multiplayer MultiplayerScreen => multiplayerScreen;
@@ -42,14 +42,18 @@ namespace osu.Game.Tests.Visual
private readonly OsuScreenStack screenStack;
private readonly TestMultiplayer multiplayerScreen;
- public TestMultiplayerScreenStack()
+ public TestMultiplayerComponents()
{
multiplayerScreen = new TestMultiplayer();
InternalChildren = new Drawable[]
{
Client = new TestMultiplayerClient(RoomManager),
- screenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both }
+ screenStack = new OsuScreenStack
+ {
+ Name = nameof(TestMultiplayerComponents),
+ RelativeSizeAxes = Axes.Both
+ }
};
screenStack.Push(multiplayerScreen);
@@ -61,7 +65,7 @@ namespace osu.Game.Tests.Visual
((DummyAPIAccess)api).HandleRequest = request => multiplayerScreen.RequestsHandler.HandleRequest(request, api.LocalUser.Value, game);
}
- public override bool OnBackButton() => multiplayerScreen.OnBackButton();
+ public override bool OnBackButton() => (screenStack.CurrentScreen as OsuScreen)?.OnBackButton() ?? base.OnBackButton();
public override bool OnExiting(IScreen next)
{
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs
index e5bcc08924..ede89c6096 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs
@@ -135,6 +135,35 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("bpm is default", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 60));
}
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestEarlyActivationEffectPoint(bool earlyActivating)
+ {
+ double earlyActivationMilliseconds = earlyActivating ? 100 : 0;
+ ControlPoint actualEffectPoint = null;
+
+ AddStep($"set early activation to {earlyActivationMilliseconds}", () => beatContainer.EarlyActivationMilliseconds = earlyActivationMilliseconds);
+
+ AddStep("seek before kiai effect point", () =>
+ {
+ ControlPoint expectedEffectPoint = Beatmap.Value.Beatmap.ControlPointInfo.EffectPoints.First(ep => ep.KiaiMode);
+ actualEffectPoint = null;
+ beatContainer.AllowMistimedEventFiring = false;
+
+ beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) =>
+ {
+ if (Precision.AlmostEquals(gameplayClockContainer.CurrentTime + earlyActivationMilliseconds, expectedEffectPoint.Time, BeatSyncedContainer.MISTIMED_ALLOWANCE))
+ actualEffectPoint = effectControlPoint;
+ };
+
+ gameplayClockContainer.Seek(expectedEffectPoint.Time - earlyActivationMilliseconds);
+ });
+
+ AddUntilStep("wait for effect point", () => actualEffectPoint != null);
+
+ AddAssert("effect has kiai", () => actualEffectPoint != null && ((EffectControlPoint)actualEffectPoint).KiaiMode);
+ }
+
private class TestBeatSyncedContainer : BeatSyncedContainer
{
private const int flash_layer_height = 150;
@@ -145,6 +174,12 @@ namespace osu.Game.Tests.Visual.UserInterface
set => base.AllowMistimedEventFiring = value;
}
+ public new double EarlyActivationMilliseconds
+ {
+ get => base.EarlyActivationMilliseconds;
+ set => base.EarlyActivationMilliseconds = value;
+ }
+
private readonly InfoString timingPointCount;
private readonly InfoString currentTimingPoint;
private readonly InfoString beatCount;
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
index 9f0f4a6b8b..2363bbbfcf 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
@@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
var score = new ScoreInfo
{
- OnlineScoreID = i,
+ OnlineID = i,
BeatmapInfo = beatmapInfo,
BeatmapInfoID = beatmapInfo.ID,
Accuracy = RNG.NextDouble(),
@@ -163,7 +163,7 @@ namespace osu.Game.Tests.Visual.UserInterface
});
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
- AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID));
+ AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID));
}
[Test]
@@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
- AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID));
+ AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != importedScores[0].OnlineID));
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs
index d30f1e8889..3fa9b8b877 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs
@@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual.UserInterface
private BeatmapSetInfo testBeatmap;
private IAPIProvider api;
- [Resolved]
- private BeatmapManager beatmaps { get; set; }
-
[BackgroundDependencyLoader]
private void load(OsuGameBase osu, IAPIProvider api, RulesetStore rulesets)
{
diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs
index 8139387a96..b678f69b8f 100644
--- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs
+++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs
@@ -6,19 +6,20 @@ using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Rulesets;
+using osu.Game.Tests.Visual;
using osu.Game.Tournament.Components;
namespace osu.Game.Tournament.Tests.Components
{
public class TestSceneTournamentBeatmapPanel : TournamentTestScene
{
+ ///
+ /// Warning: the below API instance is actually the online API, rather than the dummy API provided by the test.
+ /// It cannot be trivially replaced because setting to causes to no longer be usable.
+ ///
[Resolved]
private IAPIProvider api { get; set; }
- [Resolved]
- private RulesetStore rulesets { get; set; }
-
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs
index 3cd13df0d3..9feef36a02 100644
--- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs
+++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Tournament.Tests.Components
private IAPIProvider api { get; set; }
[Resolved]
- private RulesetStore rulesets { get; set; }
+ private IRulesetStore rulesets { get; set; }
private FillFlowContainer fillFlow;
diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs
index d149ec145b..3619aae7e0 100644
--- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs
+++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs
@@ -96,7 +96,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
Directory.CreateDirectory(flagsPath);
// Define testing files corresponding to the specific file migrations that are needed
- string bracketFile = Path.Combine(osuRoot, "bracket.json");
+ string bracketFile = Path.Combine(osuRoot, TournamentGameBase.BRACKET_FILENAME);
string drawingsConfig = Path.Combine(osuRoot, "drawings.ini");
string drawingsFile = Path.Combine(osuRoot, "drawings.txt");
@@ -133,7 +133,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
Assert.That(storage.GetFullPath("."), Is.EqualTo(migratedPath));
- Assert.True(storage.Exists("bracket.json"));
+ Assert.True(storage.Exists(TournamentGameBase.BRACKET_FILENAME));
Assert.True(storage.Exists("drawings.txt"));
Assert.True(storage.Exists("drawings_results.txt"));
diff --git a/osu.Game.Tournament/Components/TournamentModIcon.cs b/osu.Game.Tournament/Components/TournamentModIcon.cs
index 0fde263bc8..ed8a36c220 100644
--- a/osu.Game.Tournament/Components/TournamentModIcon.cs
+++ b/osu.Game.Tournament/Components/TournamentModIcon.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Tournament.Components
private readonly string modAcronym;
[Resolved]
- private RulesetStore rulesets { get; set; }
+ private IRulesetStore rulesets { get; set; }
public TournamentModIcon(string modAcronym)
{
diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs
index 02cf567837..347d368a04 100644
--- a/osu.Game.Tournament/IO/TournamentStorage.cs
+++ b/osu.Game.Tournament/IO/TournamentStorage.cs
@@ -86,7 +86,7 @@ namespace osu.Game.Tournament.IO
DeleteRecursive(source);
}
- moveFileIfExists("bracket.json", destination);
+ moveFileIfExists(TournamentGameBase.BRACKET_FILENAME, destination);
moveFileIfExists("drawings.txt", destination);
moveFileIfExists("drawings_results.txt", destination);
moveFileIfExists("drawings.ini", destination);
diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs
index a57f9fd691..5278d538d2 100644
--- a/osu.Game.Tournament/IPC/FileBasedIPC.cs
+++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Tournament.IPC
protected IAPIProvider API { get; private set; }
[Resolved]
- protected RulesetStore Rulesets { get; private set; }
+ protected IRulesetStore Rulesets { get; private set; }
[Resolved]
private GameHost host { get; set; }
diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
index 9abf1d3adb..5d2fddffd9 100644
--- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
@@ -25,9 +25,6 @@ namespace osu.Game.Tournament.Screens.Editors
protected override BindableList Storage => team.SeedingResults;
- [Resolved(canBeNull: true)]
- private TournamentSceneManager sceneManager { get; set; }
-
public SeedingEditorScreen(TournamentTeam team, TournamentScreen parentScreen)
: base(parentScreen)
{
@@ -38,9 +35,6 @@ namespace osu.Game.Tournament.Screens.Editors
{
public SeedingResult Model { get; }
- [Resolved]
- private LadderInfo ladderInfo { get; set; }
-
public SeedingResultRow(TournamentTeam team, SeedingResult round)
{
Model = round;
diff --git a/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs
index 8e9b32231f..5a1ceecd01 100644
--- a/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs
+++ b/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs
@@ -21,9 +21,6 @@ namespace osu.Game.Tournament.Screens.Setup
{
public class StablePathSelectScreen : TournamentScreen
{
- [Resolved]
- private GameHost host { get; set; }
-
[Resolved(canBeNull: true)]
private TournamentSceneManager sceneManager { get; set; }
diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs
index f03f815b83..5d613894d4 100644
--- a/osu.Game.Tournament/TournamentGame.cs
+++ b/osu.Game.Tournament/TournamentGame.cs
@@ -71,7 +71,7 @@ namespace osu.Game.Tournament
loadingSpinner.Expire();
Logger.Error(t.Exception, "Couldn't load bracket with error");
- Add(new WarningBox("Your bracket.json file could not be parsed. Please check runtime.log for more details."));
+ Add(new WarningBox($"Your {BRACKET_FILENAME} file could not be parsed. Please check runtime.log for more details."));
});
return;
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index d2f146c4c2..d08322a3e8 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.IO.Stores;
+using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Graphics;
using osu.Game.Online.API.Requests;
@@ -26,15 +27,15 @@ namespace osu.Game.Tournament
[Cached(typeof(TournamentGameBase))]
public class TournamentGameBase : OsuGameBase
{
- private const string bracket_filename = "bracket.json";
+ public const string BRACKET_FILENAME = @"bracket.json";
private LadderInfo ladder;
private TournamentStorage storage;
private DependencyContainer dependencies;
private FileBasedIPC ipc;
- protected Task BracketLoadTask => taskCompletionSource.Task;
+ protected Task BracketLoadTask => bracketLoadTaskCompletionSource.Task;
- private readonly TaskCompletionSource taskCompletionSource = new TaskCompletionSource();
+ private readonly TaskCompletionSource bracketLoadTaskCompletionSource = new TaskCompletionSource();
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
@@ -71,9 +72,9 @@ namespace osu.Game.Tournament
{
try
{
- if (storage.Exists(bracket_filename))
+ if (storage.Exists(BRACKET_FILENAME))
{
- using (Stream stream = storage.GetStream(bracket_filename, FileAccess.Read, FileMode.Open))
+ using (Stream stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Read, FileMode.Open))
using (var sr = new StreamReader(stream))
ladder = JsonConvert.DeserializeObject(sr.ReadToEnd(), new JsonPointConverter());
}
@@ -144,7 +145,7 @@ namespace osu.Game.Tournament
}
catch (Exception e)
{
- taskCompletionSource.SetException(e);
+ bracketLoadTaskCompletionSource.SetException(e);
return;
}
@@ -156,7 +157,7 @@ namespace osu.Game.Tournament
dependencies.CacheAs(ipc = new FileBasedIPC());
Add(ipc);
- taskCompletionSource.SetResult(true);
+ bracketLoadTaskCompletionSource.SetResult(true);
initialisationText.Expire();
});
@@ -292,6 +293,12 @@ namespace osu.Game.Tournament
protected virtual void SaveChanges()
{
+ if (!bracketLoadTaskCompletionSource.Task.IsCompletedSuccessfully)
+ {
+ Logger.Log("Inhibiting bracket save as bracket parsing failed");
+ return;
+ }
+
foreach (var r in ladder.Rounds)
r.Matches = ladder.Matches.Where(p => p.Round.Value == r).Select(p => p.ID).ToList();
@@ -309,7 +316,7 @@ namespace osu.Game.Tournament
Converters = new JsonConverter[] { new JsonPointConverter() }
});
- using (var stream = storage.GetStream(bracket_filename, FileAccess.Write, FileMode.Create))
+ using (var stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Write, FileMode.Create))
using (var sw = new StreamWriter(stream))
sw.Write(serialisedLadder);
}
diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs
index e631d35180..6d56d152f1 100644
--- a/osu.Game/Audio/PreviewTrackManager.cs
+++ b/osu.Game/Audio/PreviewTrackManager.cs
@@ -18,9 +18,6 @@ namespace osu.Game.Audio
private readonly BindableDouble muteBindable = new BindableDouble();
- [Resolved]
- private AudioManager audio { get; set; }
-
private ITrackStore trackStore;
protected TrackManagerPreviewTrack CurrentTrack;
diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs
index dfd21469fa..65d1fb8286 100644
--- a/osu.Game/Beatmaps/BeatmapDifficulty.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs
@@ -15,6 +15,8 @@ namespace osu.Game.Beatmaps
public int ID { get; set; }
+ public bool IsManaged => ID > 0;
+
public float DrainRate { get; set; } = DEFAULT_DIFFICULTY;
public float CircleSize { get; set; } = DEFAULT_DIFFICULTY;
public float OverallDifficulty { get; set; } = DEFAULT_DIFFICULTY;
diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
index 61717c18d5..119906cadc 100644
--- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
@@ -135,7 +135,7 @@ namespace osu.Game.Beatmaps
var localRulesetInfo = rulesetInfo as RulesetInfo;
// Difficulty can only be computed if the beatmap and ruleset are locally available.
- if (localBeatmapInfo == null || localBeatmapInfo.ID == 0 || localRulesetInfo == null)
+ if (localBeatmapInfo?.IsManaged != true || localRulesetInfo == null)
{
// If not, fall back to the existing star difficulty (e.g. from an online source).
return Task.FromResult(new StarDifficulty(beatmapInfo.StarRating, (beatmapInfo as IBeatmapOnlineInfo)?.MaxCombo ?? 0));
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 36f82ac56c..4175d7ff6b 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -20,6 +20,8 @@ namespace osu.Game.Beatmaps
{
public int ID { get; set; }
+ public bool IsManaged => ID > 0;
+
public int BeatmapVersion;
private int? onlineID;
diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs
index b395f16c24..5da0264893 100644
--- a/osu.Game/Beatmaps/BeatmapMetadata.cs
+++ b/osu.Game/Beatmaps/BeatmapMetadata.cs
@@ -20,6 +20,8 @@ namespace osu.Game.Beatmaps
{
public int ID { get; set; }
+ public bool IsManaged => ID > 0;
+
public string Title { get; set; } = string.Empty;
[JsonProperty("title_unicode")]
diff --git a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs
index ce50463f05..29dcf4d6aa 100644
--- a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs
@@ -11,6 +11,8 @@ namespace osu.Game.Beatmaps
{
public int ID { get; set; }
+ public bool IsManaged => ID > 0;
+
public int BeatmapSetInfoID { get; set; }
public int FileInfoID { get; set; }
diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs
index ac7067edda..a3a8f8555f 100644
--- a/osu.Game/Beatmaps/BeatmapSetInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs
@@ -18,6 +18,8 @@ namespace osu.Game.Beatmaps
{
public int ID { get; set; }
+ public bool IsManaged => ID > 0;
+
private int? onlineID;
[Column("OnlineBeatmapSetID")]
diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs
index 8b00d0f7f2..3949e84f4a 100644
--- a/osu.Game/Beatmaps/DifficultyRecommender.cs
+++ b/osu.Game/Beatmaps/DifficultyRecommender.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps
private IAPIProvider api { get; set; }
[Resolved]
- private RulesetStore rulesets { get; set; }
+ private IRulesetStore rulesets { get; set; }
[Resolved]
private Bindable ruleset { get; set; }
@@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps
///
private int? requestedUserId;
- private readonly Dictionary recommendedDifficultyMapping = new Dictionary();
+ private readonly Dictionary recommendedDifficultyMapping = new Dictionary();
private readonly IBindable apiState = new Bindable();
@@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps
/// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first.
///
- private IEnumerable orderedRulesets
+ private IEnumerable orderedRulesets
{
get
{
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
index 37c1bacda4..1e24501426 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
@@ -31,10 +31,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards
public class BeatmapCard : OsuClickableContainer
{
public const float TRANSITION_DURATION = 400;
+ public const float CORNER_RADIUS = 10;
+
+ public IBindable Expanded { get; }
private const float width = 408;
private const float height = 100;
- private const float corner_radius = 10;
private const float icon_area_width = 30;
private readonly APIBeatmapSet beatmapSet;
@@ -42,6 +44,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private readonly BeatmapDownloadTracker downloadTracker;
+ private BeatmapCardContent content = null!;
+
private BeatmapCardThumbnail thumbnail = null!;
private Container rightAreaBackground = null!;
@@ -60,9 +64,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
- public BeatmapCard(APIBeatmapSet beatmapSet)
+ public BeatmapCard(APIBeatmapSet beatmapSet, bool allowExpansion = true)
: base(HoverSampleSet.Submit)
{
+ Expanded = new BindableBool { Disabled = !allowExpansion };
+
this.beatmapSet = beatmapSet;
favouriteState = new Bindable(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount));
downloadTracker = new BeatmapDownloadTracker(beatmapSet);
@@ -73,242 +79,247 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
Width = width;
Height = height;
- CornerRadius = corner_radius;
- Masking = true;
FillFlowContainer leftIconArea;
GridContainer titleContainer;
GridContainer artistContainer;
- InternalChildren = new Drawable[]
+ InternalChild = content = new BeatmapCardContent(height)
{
- downloadTracker,
- rightAreaBackground = new Container
+ MainContent = new Container
{
- RelativeSizeAxes = Axes.Y,
- Width = icon_area_width + 2 * corner_radius,
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- // workaround for masking artifacts at the top & bottom of card,
- // which become especially visible on downloaded beatmaps (when the icon area has a lime background).
- Padding = new MarginPadding { Vertical = 1 },
- Child = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Colour4.White
- },
- },
- thumbnail = new BeatmapCardThumbnail(beatmapSet)
- {
- Name = @"Left (icon) area",
- Size = new Vector2(height),
- Padding = new MarginPadding { Right = corner_radius },
- Child = leftIconArea = new FillFlowContainer
- {
- Margin = new MarginPadding(5),
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(1)
- }
- },
- new Container
- {
- Name = @"Right (button) area",
- Width = 30,
- RelativeSizeAxes = Axes.Y,
- Origin = Anchor.TopRight,
- Anchor = Anchor.TopRight,
- Padding = new MarginPadding { Vertical = 17.5f },
- Child = rightAreaButtons = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new BeatmapCardIconButton[]
- {
- new FavouriteButton(beatmapSet)
- {
- Current = favouriteState,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre
- },
- new DownloadButton(beatmapSet)
- {
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- State = { BindTarget = downloadTracker.State }
- },
- new GoToBeatmapButton(beatmapSet)
- {
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- State = { BindTarget = downloadTracker.State }
- }
- }
- }
- },
- mainContent = new Container
- {
- Name = @"Main content",
- X = height - corner_radius,
- Height = height,
- CornerRadius = corner_radius,
- Masking = true,
+ RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- mainContentBackground = new BeatmapCardContentBackground(beatmapSet)
+ downloadTracker,
+ rightAreaBackground = new Container
{
- RelativeSizeAxes = Axes.Both,
- },
- new FillFlowContainer
- {
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding
+ RelativeSizeAxes = Axes.Y,
+ Width = icon_area_width + 2 * CORNER_RADIUS,
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ // workaround for masking artifacts at the top & bottom of card,
+ // which become especially visible on downloaded beatmaps (when the icon area has a lime background).
+ Padding = new MarginPadding { Vertical = 1 },
+ Child = new Box
{
- Horizontal = 10,
- Vertical = 4
+ RelativeSizeAxes = Axes.Both,
+ Colour = Colour4.White
},
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
+ },
+ thumbnail = new BeatmapCardThumbnail(beatmapSet)
+ {
+ Name = @"Left (icon) area",
+ Size = new Vector2(height),
+ Padding = new MarginPadding { Right = CORNER_RADIUS },
+ Child = leftIconArea = new FillFlowContainer
{
- titleContainer = new GridContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- ColumnDimensions = new[]
- {
- new Dimension(),
- new Dimension(GridSizeMode.AutoSize)
- },
- RowDimensions = new[]
- {
- new Dimension(GridSizeMode.AutoSize)
- },
- Content = new[]
- {
- new[]
- {
- new OsuSpriteText
- {
- Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title),
- Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
- RelativeSizeAxes = Axes.X,
- Truncate = true
- },
- Empty()
- }
- }
- },
- artistContainer = new GridContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- ColumnDimensions = new[]
- {
- new Dimension(),
- new Dimension(GridSizeMode.AutoSize)
- },
- RowDimensions = new[]
- {
- new Dimension(GridSizeMode.AutoSize)
- },
- Content = new[]
- {
- new[]
- {
- new OsuSpriteText
- {
- Text = createArtistText(),
- Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
- RelativeSizeAxes = Axes.X,
- Truncate = true
- },
- Empty()
- },
- }
- },
- new LinkFlowContainer(s =>
- {
- s.Shadow = false;
- s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold);
- }).With(d =>
- {
- d.AutoSizeAxes = Axes.Both;
- d.Margin = new MarginPadding { Top = 2 };
- d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
- d.AddUserLink(beatmapSet.Author);
- }),
+ Margin = new MarginPadding(5),
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(1)
}
},
new Container
{
- Name = @"Bottom content",
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Padding = new MarginPadding
+ Name = @"Right (button) area",
+ Width = 30,
+ RelativeSizeAxes = Axes.Y,
+ Origin = Anchor.TopRight,
+ Anchor = Anchor.TopRight,
+ Padding = new MarginPadding { Vertical = 17.5f },
+ Child = rightAreaButtons = new Container
{
- Horizontal = 10,
- Vertical = 4
- },
+ RelativeSizeAxes = Axes.Both,
+ Children = new BeatmapCardIconButton[]
+ {
+ new FavouriteButton(beatmapSet)
+ {
+ Current = favouriteState,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre
+ },
+ new DownloadButton(beatmapSet)
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ State = { BindTarget = downloadTracker.State }
+ },
+ new GoToBeatmapButton(beatmapSet)
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ State = { BindTarget = downloadTracker.State }
+ }
+ }
+ }
+ },
+ mainContent = new Container
+ {
+ Name = @"Main content",
+ X = height - CORNER_RADIUS,
+ Height = height,
+ CornerRadius = CORNER_RADIUS,
+ Masking = true,
Children = new Drawable[]
{
- idleBottomContent = new FillFlowContainer
+ mainContentBackground = new BeatmapCardContentBackground(beatmapSet)
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding
+ {
+ Horizontal = 10,
+ Vertical = 4
+ },
Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 3),
- AlwaysPresent = true,
Children = new Drawable[]
{
- statisticsContainer = new FillFlowContainer
+ titleContainer = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(10, 0),
- Alpha = 0,
- AlwaysPresent = true,
- ChildrenEnumerable = createStatistics()
- },
- new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(4, 0),
- Children = new Drawable[]
+ ColumnDimensions = new[]
{
- new BeatmapSetOnlineStatusPill
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ Content = new[]
+ {
+ new[]
{
- AutoSizeAxes = Axes.Both,
- Status = beatmapSet.Status,
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft
- },
- new DifficultySpectrumDisplay(beatmapSet)
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- DotSize = new Vector2(6, 12)
+ new OsuSpriteText
+ {
+ Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title),
+ Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
+ RelativeSizeAxes = Axes.X,
+ Truncate = true
+ },
+ Empty()
}
}
- }
+ },
+ artistContainer = new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ ColumnDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ Content = new[]
+ {
+ new[]
+ {
+ new OsuSpriteText
+ {
+ Text = createArtistText(),
+ Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
+ RelativeSizeAxes = Axes.X,
+ Truncate = true
+ },
+ Empty()
+ },
+ }
+ },
+ new LinkFlowContainer(s =>
+ {
+ s.Shadow = false;
+ s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold);
+ }).With(d =>
+ {
+ d.AutoSizeAxes = Axes.Both;
+ d.Margin = new MarginPadding { Top = 2 };
+ d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
+ d.AddUserLink(beatmapSet.Author);
+ }),
}
},
- downloadProgressBar = new BeatmapCardDownloadProgressBar
+ new Container
{
+ Name = @"Bottom content",
RelativeSizeAxes = Axes.X,
- Height = 6,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- State = { BindTarget = downloadTracker.State },
- Progress = { BindTarget = downloadTracker.Progress }
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Padding = new MarginPadding
+ {
+ Horizontal = 10,
+ Vertical = 4
+ },
+ Children = new Drawable[]
+ {
+ idleBottomContent = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 3),
+ AlwaysPresent = true,
+ Children = new Drawable[]
+ {
+ statisticsContainer = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(10, 0),
+ Alpha = 0,
+ AlwaysPresent = true,
+ ChildrenEnumerable = createStatistics()
+ },
+ new BeatmapCardExtraInfoRow(beatmapSet)
+ {
+ Hovered = _ =>
+ {
+ content.ExpandAfterDelay();
+ return false;
+ },
+ Unhovered = _ =>
+ {
+ // Handles the case where a user has not shown explicit intent to view expanded info.
+ // ie. quickly moved over the info row area but didn't remain within it.
+ if (!Expanded.Value)
+ content.CancelExpand();
+ }
+ }
+ }
+ },
+ downloadProgressBar = new BeatmapCardDownloadProgressBar
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 6,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ State = { BindTarget = downloadTracker.State },
+ Progress = { BindTarget = downloadTracker.Progress }
+ }
+ }
}
}
}
}
- }
+ },
+ ExpandedContent = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = 10, Vertical = 13 },
+ Child = new BeatmapCardDifficultyList(beatmapSet)
+ },
+ Expanded = { BindTarget = Expanded }
};
if (beatmapSet.HasVideo)
@@ -344,7 +355,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
base.LoadComplete();
- downloadTracker.State.BindValueChanged(_ => updateState(), true);
+ downloadTracker.State.BindValueChanged(_ => updateState());
+ Expanded.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
}
@@ -386,19 +398,25 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private void updateState()
{
- float targetWidth = width - height;
- if (IsHovered)
- targetWidth = targetWidth - icon_area_width + corner_radius;
+ bool showDetails = IsHovered || Expanded.Value;
- thumbnail.Dimmed.Value = IsHovered;
+ float targetWidth = width - height;
+ if (showDetails)
+ targetWidth = targetWidth - icon_area_width + CORNER_RADIUS;
+
+ thumbnail.Dimmed.Value = showDetails;
+
+ // Scale value is intentionally chosen to fit in the spacing of listing displays, as to not overlap horizontally with adjacent cards.
+ // This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left.
+ content.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint);
mainContent.ResizeWidthTo(targetWidth, TRANSITION_DURATION, Easing.OutQuint);
- mainContentBackground.Dimmed.Value = IsHovered;
+ mainContentBackground.Dimmed.Value = showDetails;
- statisticsContainer.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
+ statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
rightAreaBackground.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, TRANSITION_DURATION, Easing.OutQuint);
- rightAreaButtons.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
+ rightAreaButtons.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
foreach (var button in rightAreaButtons)
{
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs
new file mode 100644
index 0000000000..286e03e700
--- /dev/null
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs
@@ -0,0 +1,205 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Framework.Threading;
+using osu.Framework.Utils;
+using osu.Game.Graphics.Containers;
+using osu.Game.Overlays;
+using osuTK;
+
+namespace osu.Game.Beatmaps.Drawables.Cards
+{
+ public class BeatmapCardContent : CompositeDrawable
+ {
+ public Drawable MainContent
+ {
+ set => bodyContent.Child = value;
+ }
+
+ public Drawable ExpandedContent
+ {
+ set => dropdownScroll.Child = value;
+ }
+
+ public IBindable Expanded => expanded;
+
+ private readonly BindableBool expanded = new BindableBool();
+
+ private readonly Box background;
+ private readonly Container content;
+ private readonly Container bodyContent;
+ private readonly Container dropdownContent;
+ private readonly OsuScrollContainer dropdownScroll;
+ private readonly Container borderContainer;
+
+ public BeatmapCardContent(float height)
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = height;
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ InternalChild = content = new HoverHandlingContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ CornerRadius = BeatmapCard.CORNER_RADIUS,
+ Masking = true,
+ Unhovered = _ => updateFromHoverChange(),
+ Children = new Drawable[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ bodyContent = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = height,
+ CornerRadius = BeatmapCard.CORNER_RADIUS,
+ Masking = true,
+ },
+ dropdownContent = new HoverHandlingContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Margin = new MarginPadding { Top = height },
+ Alpha = 0,
+ Hovered = _ =>
+ {
+ updateFromHoverChange();
+ return true;
+ },
+ Unhovered = _ => updateFromHoverChange(),
+ Child = dropdownScroll = new ExpandedContentScrollContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ ScrollbarVisible = false
+ }
+ },
+ borderContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ CornerRadius = BeatmapCard.CORNER_RADIUS,
+ Masking = true,
+ BorderThickness = 3,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true
+ }
+ }
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ background.Colour = colourProvider.Background2;
+ borderContainer.BorderColour = colourProvider.Highlight1;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Expanded.BindValueChanged(_ => updateState(), true);
+ FinishTransforms(true);
+ }
+
+ private ScheduledDelegate? scheduledExpandedChange;
+
+ public void ExpandAfterDelay() => queueExpandedStateChange(true, 100);
+
+ public void CancelExpand() => scheduledExpandedChange?.Cancel();
+
+ private void updateFromHoverChange() =>
+ queueExpandedStateChange(content.IsHovered || dropdownContent.IsHovered, 100);
+
+ private void queueExpandedStateChange(bool newState, int delay = 0)
+ {
+ if (Expanded.Disabled)
+ return;
+
+ scheduledExpandedChange?.Cancel();
+ scheduledExpandedChange = Scheduler.AddDelayed(() => expanded.Value = newState, delay);
+ }
+
+ private void updateState()
+ {
+ background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+ dropdownContent.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+ borderContainer.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+
+ content.TweenEdgeEffectTo(new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Offset = new Vector2(0, 2),
+ Radius = 10,
+ Colour = Colour4.Black.Opacity(Expanded.Value ? 0.3f : 0f),
+ Hollow = true,
+ }, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+ }
+
+ private class ExpandedContentScrollContainer : OsuScrollContainer
+ {
+ public ExpandedContentScrollContainer()
+ {
+ ScrollbarVisible = false;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ Height = Math.Min(Content.DrawHeight, 400);
+ }
+
+ private bool allowScroll => !Precision.AlmostEquals(DrawSize, Content.DrawSize);
+
+ protected override bool OnDragStart(DragStartEvent e)
+ {
+ if (!allowScroll)
+ return false;
+
+ return base.OnDragStart(e);
+ }
+
+ protected override void OnDrag(DragEvent e)
+ {
+ if (!allowScroll)
+ return;
+
+ base.OnDrag(e);
+ }
+
+ protected override void OnDragEnd(DragEndEvent e)
+ {
+ if (!allowScroll)
+ return;
+
+ base.OnDragEnd(e);
+ }
+
+ protected override bool OnScroll(ScrollEvent e)
+ {
+ if (!allowScroll)
+ return false;
+
+ return base.OnScroll(e);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs
new file mode 100644
index 0000000000..7753d8480a
--- /dev/null
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs
@@ -0,0 +1,103 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.Chat;
+using osu.Game.Rulesets;
+using osuTK;
+
+namespace osu.Game.Beatmaps.Drawables.Cards
+{
+ public class BeatmapCardDifficultyList : CompositeDrawable
+ {
+ public BeatmapCardDifficultyList(IBeatmapSetInfo beatmapSetInfo)
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ FillFlowContainer flow;
+
+ InternalChild = flow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 3)
+ };
+
+ bool firstGroup = true;
+
+ foreach (var group in beatmapSetInfo.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key))
+ {
+ if (!firstGroup)
+ {
+ flow.Add(Empty().With(s =>
+ {
+ s.RelativeSizeAxes = Axes.X;
+ s.Height = 4;
+ }));
+ }
+
+ foreach (var difficulty in group.OrderBy(b => b.StarRating))
+ flow.Add(new BeatmapCardDifficultyRow(difficulty));
+
+ firstGroup = false;
+ }
+ }
+
+ private class BeatmapCardDifficultyRow : CompositeDrawable
+ {
+ private readonly IBeatmapInfo beatmapInfo;
+
+ public BeatmapCardDifficultyRow(IBeatmapInfo beatmapInfo)
+ {
+ this.beatmapInfo = beatmapInfo;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(RulesetStore rulesets)
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ InternalChild = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(4, 0),
+ Children = new[]
+ {
+ (rulesets.GetRuleset(beatmapInfo.Ruleset.OnlineID)?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }).With(icon =>
+ {
+ icon.Anchor = icon.Origin = Anchor.CentreLeft;
+ icon.Size = new Vector2(16);
+ }),
+ new StarRatingDisplay(new StarDifficulty(beatmapInfo.StarRating, 0), StarRatingDisplaySize.Small)
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft
+ },
+ new LinkFlowContainer(s =>
+ {
+ s.Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold);
+ }).With(d =>
+ {
+ d.AutoSizeAxes = Axes.Both;
+ d.Anchor = Anchor.CentreLeft;
+ d.Origin = Anchor.CentreLeft;
+ d.Padding = new MarginPadding { Bottom = 2 };
+ d.AddLink(beatmapInfo.DifficultyName, LinkAction.OpenBeatmap, beatmapInfo.OnlineID.ToString());
+ })
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs
new file mode 100644
index 0000000000..0a9d98e621
--- /dev/null
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs
@@ -0,0 +1,43 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Online.API.Requests.Responses;
+using osuTK;
+
+namespace osu.Game.Beatmaps.Drawables.Cards
+{
+ public class BeatmapCardExtraInfoRow : HoverHandlingContainer
+ {
+ public BeatmapCardExtraInfoRow(APIBeatmapSet beatmapSet)
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ Child = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(4, 0),
+ Children = new Drawable[]
+ {
+ new BeatmapSetOnlineStatusPill
+ {
+ AutoSizeAxes = Axes.Both,
+ Status = beatmapSet.Status,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft
+ },
+ new DifficultySpectrumDisplay(beatmapSet)
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ DotSize = new Vector2(6, 12)
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/HoverHandlingContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/HoverHandlingContainer.cs
new file mode 100644
index 0000000000..fe37616755
--- /dev/null
+++ b/osu.Game/Beatmaps/Drawables/Cards/HoverHandlingContainer.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using System;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Events;
+
+namespace osu.Game.Beatmaps.Drawables.Cards
+{
+ public class HoverHandlingContainer : Container
+ {
+ public Func? Hovered { get; set; }
+ public Action? Unhovered { get; set; }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ bool handledByBase = base.OnHover(e);
+ return Hovered?.Invoke(e) ?? handledByBase;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ base.OnHoverLost(e);
+ Unhovered?.Invoke(e);
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
index 6e573cc2a0..82be0559a7 100644
--- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
+++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
@@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps.Drawables
}
[Resolved]
- private RulesetStore rulesets { get; set; }
+ private IRulesetStore rulesets { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs
index f4501f0633..5b211084ab 100644
--- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs
+++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs
@@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps.Drawables
// matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127
bool collapsed = beatmapSet.Beatmaps.Count() > 12;
- foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID))
+ foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key))
{
flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key, rulesetGrouping, collapsed));
}
diff --git a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
new file mode 100644
index 0000000000..8c915e2872
--- /dev/null
+++ b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
@@ -0,0 +1,52 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.IO;
+using osu.Framework.Audio.Track;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Beatmaps.Formats;
+using osu.Game.IO;
+using osu.Game.Rulesets;
+using osu.Game.Skinning;
+
+namespace osu.Game.Beatmaps
+{
+ ///
+ /// A which can be constructed directly from a .osu file, providing an implementation for
+ /// .
+ ///
+ public class FlatFileWorkingBeatmap : WorkingBeatmap
+ {
+ private readonly Beatmap beatmap;
+
+ public FlatFileWorkingBeatmap(string file, Func rulesetProvider, int? beatmapId = null)
+ : this(readFromFile(file), rulesetProvider, beatmapId)
+ {
+ }
+
+ private FlatFileWorkingBeatmap(Beatmap beatmap, Func rulesetProvider, int? beatmapId = null)
+ : base(beatmap.BeatmapInfo, null)
+ {
+ this.beatmap = beatmap;
+
+ beatmap.BeatmapInfo.Ruleset = rulesetProvider(beatmap.BeatmapInfo.RulesetID).RulesetInfo;
+
+ if (beatmapId.HasValue)
+ beatmap.BeatmapInfo.OnlineID = beatmapId;
+ }
+
+ private static Beatmap readFromFile(string filename)
+ {
+ using (var stream = File.OpenRead(filename))
+ using (var reader = new LineBufferedReader(stream))
+ return Decoder.GetDecoder(reader).Decode(reader);
+ }
+
+ protected override IBeatmap GetBeatmap() => beatmap;
+ protected override Texture GetBackground() => throw new NotImplementedException();
+ protected override Track GetBeatmapTrack() => throw new NotImplementedException();
+ protected internal override ISkin GetSkin() => throw new NotImplementedException();
+ public override Stream GetStream(string storagePath) => throw new NotImplementedException();
+ }
+}
diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
index 559685d3c4..449406eadf 100644
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -15,6 +15,7 @@ using osu.Framework.Platform;
using osu.Framework.Statistics;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Formats;
+using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Skinning;
using osu.Game.Storyboards;
@@ -108,6 +109,7 @@ namespace osu.Game.Beatmaps
TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore;
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
AudioManager IStorageResourceProvider.AudioManager => audioManager;
+ RealmContextFactory IStorageResourceProvider.RealmContextFactory => null;
IResourceStore IStorageResourceProvider.Files => files;
IResourceStore IStorageResourceProvider.Resources => resources;
IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore);
diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs
index ad23874b2e..77bda00107 100644
--- a/osu.Game/Collections/CollectionFilterDropdown.cs
+++ b/osu.Game/Collections/CollectionFilterDropdown.cs
@@ -13,7 +13,6 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
-using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osuTK;
@@ -193,9 +192,6 @@ namespace osu.Game.Collections
[NotNull]
protected new CollectionFilterMenuItem Item => ((DropdownMenuItem)base.Item).Value;
- [Resolved]
- private OsuColour colours { get; set; }
-
[Resolved]
private IBindable beatmap { get; set; }
diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs
index 9ff92032b7..c4f991094c 100644
--- a/osu.Game/Collections/CollectionManager.cs
+++ b/osu.Game/Collections/CollectionManager.cs
@@ -40,9 +40,6 @@ namespace osu.Game.Collections
public readonly BindableList Collections = new BindableList();
- [Resolved]
- private GameHost host { get; set; }
-
[Resolved]
private BeatmapManager beatmaps { get; set; }
diff --git a/osu.Game/Configuration/DatabasedSetting.cs b/osu.Game/Configuration/DatabasedSetting.cs
index fe1d51d57f..65d9f7799d 100644
--- a/osu.Game/Configuration/DatabasedSetting.cs
+++ b/osu.Game/Configuration/DatabasedSetting.cs
@@ -11,6 +11,8 @@ namespace osu.Game.Configuration
{
public int ID { get; set; }
+ public bool IsManaged => ID > 0;
+
public int? RulesetID { get; set; }
public int? Variant { get; set; }
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 84da3f666d..c6a2abecd7 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -17,6 +17,7 @@ using osu.Game.Overlays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
+using osu.Game.Skinning;
namespace osu.Game.Configuration
{
@@ -27,7 +28,7 @@ namespace osu.Game.Configuration
{
// UI/selection defaults
SetDefault(OsuSetting.Ruleset, string.Empty);
- SetDefault(OsuSetting.Skin, 0, -1, int.MaxValue);
+ SetDefault(OsuSetting.Skin, SkinInfo.DEFAULT_SKIN.ToString());
SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details);
SetDefault(OsuSetting.BeatmapDetailModsFilter, false);
@@ -210,9 +211,12 @@ namespace osu.Game.Configuration
value: scalingMode.GetLocalisableDescription()
)
),
- new TrackedSetting(OsuSetting.Skin, skin =>
+ new TrackedSetting(OsuSetting.Skin, skin =>
{
- string skinName = LookupSkinName(skin) ?? string.Empty;
+ string skinName = string.Empty;
+
+ if (Guid.TryParse(skin, out var id))
+ skinName = LookupSkinName(id) ?? string.Empty;
return new SettingDescription(
rawValue: skinName,
@@ -233,7 +237,7 @@ namespace osu.Game.Configuration
};
}
- public Func LookupSkinName { private get; set; }
+ public Func LookupSkinName { private get; set; }
public Func LookupKeyBindings { get; set; }
}
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index 8cabb55cc3..9c26451d40 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -476,7 +476,7 @@ namespace osu.Game.Database
{
Files.Dereference(file.FileInfo);
- if (file.ID > 0)
+ if (file.IsManaged)
{
// This shouldn't be required, but here for safety in case the provided TModel is not being change tracked
// Definitely can be removed once we rework the database backend.
@@ -505,7 +505,7 @@ namespace osu.Game.Database
});
}
- if (model.ID > 0)
+ if (model.IsManaged)
Update(model);
}
@@ -737,7 +737,7 @@ namespace osu.Game.Database
/// The usable items present in the store.
/// Whether the exists.
protected virtual bool CheckLocalAvailability(TModel model, IQueryable items)
- => model.ID > 0 && items.Any(i => i.ID == model.ID && i.Files.Any());
+ => model.IsManaged && items.Any(i => i.ID == model.ID && i.Files.Any());
///
/// Whether import can be skipped after finding an existing import early in the process.
diff --git a/osu.Game/Database/BeatmapLookupCache.cs b/osu.Game/Database/BeatmapLookupCache.cs
new file mode 100644
index 0000000000..c6f8244494
--- /dev/null
+++ b/osu.Game/Database/BeatmapLookupCache.cs
@@ -0,0 +1,149 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Database
+{
+ // This class is based on `UserLookupCache` which is well tested.
+ // If modifications are to be made here, a base abstract implementation should likely be created and shared between the two.
+ public class BeatmapLookupCache : MemoryCachingComponent
+ {
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ ///
+ /// Perform an API lookup on the specified beatmap, populating a model.
+ ///
+ /// The beatmap to lookup.
+ /// An optional cancellation token.
+ /// The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied.
+ [ItemCanBeNull]
+ public Task GetBeatmapAsync(int beatmapId, CancellationToken token = default) => GetAsync(beatmapId, token);
+
+ ///
+ /// Perform an API lookup on the specified beatmaps, populating a model.
+ ///
+ /// The beatmaps to lookup.
+ /// An optional cancellation token.
+ /// The populated beatmaps. May include null results for failed retrievals.
+ public Task GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default)
+ {
+ var beatmapLookupTasks = new List>();
+
+ foreach (int u in beatmapIds)
+ {
+ beatmapLookupTasks.Add(GetBeatmapAsync(u, token).ContinueWith(task =>
+ {
+ if (!task.IsCompletedSuccessfully)
+ return null;
+
+ return task.Result;
+ }, token));
+ }
+
+ return Task.WhenAll(beatmapLookupTasks);
+ }
+
+ protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default)
+ => await queryBeatmap(lookup).ConfigureAwait(false);
+
+ private readonly Queue<(int id, TaskCompletionSource)> pendingBeatmapTasks = new Queue<(int, TaskCompletionSource)>();
+ private Task pendingRequestTask;
+ private readonly object taskAssignmentLock = new object();
+
+ private Task queryBeatmap(int beatmapId)
+ {
+ lock (taskAssignmentLock)
+ {
+ var tcs = new TaskCompletionSource();
+
+ // Add to the queue.
+ pendingBeatmapTasks.Enqueue((beatmapId, tcs));
+
+ // Create a request task if there's not already one.
+ if (pendingRequestTask == null)
+ createNewTask();
+
+ return tcs.Task;
+ }
+ }
+
+ private void performLookup()
+ {
+ // contains at most 50 unique beatmap IDs from beatmapTasks, which is used to perform the lookup.
+ var beatmapTasks = new Dictionary>>();
+
+ // Grab at most 50 unique beatmap IDs from the queue.
+ lock (taskAssignmentLock)
+ {
+ while (pendingBeatmapTasks.Count > 0 && beatmapTasks.Count < 50)
+ {
+ (int id, TaskCompletionSource task) next = pendingBeatmapTasks.Dequeue();
+
+ // Perform a secondary check for existence, in case the beatmap was queried in a previous batch.
+ if (CheckExists(next.id, out var existing))
+ next.task.SetResult(existing);
+ else
+ {
+ if (beatmapTasks.TryGetValue(next.id, out var tasks))
+ tasks.Add(next.task);
+ else
+ beatmapTasks[next.id] = new List> { next.task };
+ }
+ }
+ }
+
+ if (beatmapTasks.Count == 0)
+ return;
+
+ // Query the beatmaps.
+ var request = new GetBeatmapsRequest(beatmapTasks.Keys.ToArray());
+
+ // rather than queueing, we maintain our own single-threaded request stream.
+ // todo: we probably want retry logic here.
+ api.Perform(request);
+
+ // Create a new request task if there's still more beatmaps to query.
+ lock (taskAssignmentLock)
+ {
+ pendingRequestTask = null;
+ if (pendingBeatmapTasks.Count > 0)
+ createNewTask();
+ }
+
+ List foundBeatmaps = request.Response?.Beatmaps;
+
+ if (foundBeatmaps != null)
+ {
+ foreach (var beatmap in foundBeatmaps)
+ {
+ if (beatmapTasks.TryGetValue(beatmap.OnlineID, out var tasks))
+ {
+ foreach (var task in tasks)
+ task.SetResult(beatmap);
+
+ beatmapTasks.Remove(beatmap.OnlineID);
+ }
+ }
+ }
+
+ // if any tasks remain which were not satisfied, return null.
+ foreach (var tasks in beatmapTasks.Values)
+ {
+ foreach (var task in tasks)
+ task.SetResult(null);
+ }
+ }
+
+ private void createNewTask() => pendingRequestTask = Task.Run(performLookup);
+ }
+}
diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs
new file mode 100644
index 0000000000..b79a982460
--- /dev/null
+++ b/osu.Game/Database/EFToRealmMigrator.cs
@@ -0,0 +1,148 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using Microsoft.EntityFrameworkCore;
+using osu.Game.Configuration;
+using osu.Game.Models;
+using osu.Game.Skinning;
+
+#nullable enable
+
+namespace osu.Game.Database
+{
+ internal class EFToRealmMigrator
+ {
+ private readonly DatabaseContextFactory efContextFactory;
+ private readonly RealmContextFactory realmContextFactory;
+ private readonly OsuConfigManager config;
+
+ public EFToRealmMigrator(DatabaseContextFactory efContextFactory, RealmContextFactory realmContextFactory, OsuConfigManager config)
+ {
+ this.efContextFactory = efContextFactory;
+ this.realmContextFactory = realmContextFactory;
+ this.config = config;
+ }
+
+ public void Run()
+ {
+ using (var db = efContextFactory.GetForWrite())
+ {
+ migrateSettings(db);
+ migrateSkins(db);
+ }
+ }
+
+ private void migrateSkins(DatabaseWriteUsage db)
+ {
+ // can be removed 20220530.
+ var existingSkins = db.Context.SkinInfo
+ .Include(s => s.Files)
+ .ThenInclude(f => f.FileInfo)
+ .ToList();
+
+ // previous entries in EF are removed post migration.
+ if (!existingSkins.Any())
+ return;
+
+ var userSkinChoice = config.GetBindable(OsuSetting.Skin);
+ int.TryParse(userSkinChoice.Value, out int userSkinInt);
+
+ switch (userSkinInt)
+ {
+ case EFSkinInfo.DEFAULT_SKIN:
+ userSkinChoice.Value = SkinInfo.DEFAULT_SKIN.ToString();
+ break;
+
+ case EFSkinInfo.CLASSIC_SKIN:
+ userSkinChoice.Value = SkinInfo.CLASSIC_SKIN.ToString();
+ break;
+ }
+
+ using (var realm = realmContextFactory.CreateContext())
+ using (var transaction = realm.BeginWrite())
+ {
+ // only migrate data if the realm database is empty.
+ // note that this cannot be written as: `realm.All().All(s => s.Protected)`, because realm does not support `.All()`.
+ if (!realm.All().Any(s => !s.Protected))
+ {
+ foreach (var skin in existingSkins)
+ {
+ var realmSkin = new SkinInfo
+ {
+ Name = skin.Name,
+ Creator = skin.Creator,
+ Hash = skin.Hash,
+ Protected = false,
+ InstantiationInfo = skin.InstantiationInfo,
+ };
+
+ foreach (var file in skin.Files)
+ {
+ var realmFile = realm.Find(file.FileInfo.Hash);
+
+ if (realmFile == null)
+ realm.Add(realmFile = new RealmFile { Hash = file.FileInfo.Hash });
+
+ realmSkin.Files.Add(new RealmNamedFileUsage(realmFile, file.Filename));
+ }
+
+ realm.Add(realmSkin);
+
+ if (skin.ID == userSkinInt)
+ userSkinChoice.Value = realmSkin.ID.ToString();
+ }
+ }
+
+ db.Context.RemoveRange(existingSkins);
+ // Intentionally don't clean up the files, so they don't get purged by EF.
+
+ transaction.Commit();
+ }
+ }
+
+ private void migrateSettings(DatabaseWriteUsage db)
+ {
+ // migrate ruleset settings. can be removed 20220315.
+ var existingSettings = db.Context.DatabasedSetting;
+
+ // previous entries in EF are removed post migration.
+ if (!existingSettings.Any())
+ return;
+
+ using (var realm = realmContextFactory.CreateContext())
+ using (var transaction = realm.BeginWrite())
+ {
+ // only migrate data if the realm database is empty.
+ if (!realm.All().Any())
+ {
+ foreach (var dkb in existingSettings)
+ {
+ if (dkb.RulesetID == null)
+ continue;
+
+ string? shortName = getRulesetShortNameFromLegacyID(dkb.RulesetID.Value);
+
+ if (string.IsNullOrEmpty(shortName))
+ continue;
+
+ realm.Add(new RealmRulesetSetting
+ {
+ Key = dkb.Key,
+ Value = dkb.StringValue,
+ RulesetName = shortName,
+ Variant = dkb.Variant ?? 0,
+ });
+ }
+ }
+
+ db.Context.RemoveRange(existingSettings);
+
+ transaction.Commit();
+ }
+ }
+
+ private string? getRulesetShortNameFromLegacyID(long rulesetId) =>
+ efContextFactory.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName;
+ }
+}
diff --git a/osu.Game/Database/IHasPrimaryKey.cs b/osu.Game/Database/IHasPrimaryKey.cs
index 3c0fc94418..51a49948fe 100644
--- a/osu.Game/Database/IHasPrimaryKey.cs
+++ b/osu.Game/Database/IHasPrimaryKey.cs
@@ -11,5 +11,7 @@ namespace osu.Game.Database
[JsonIgnore]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
int ID { get; set; }
+
+ bool IsManaged { get; }
}
}
diff --git a/osu.Game/Database/ILive.cs b/osu.Game/Database/ILive.cs
index a863339f11..3011754bc1 100644
--- a/osu.Game/Database/ILive.cs
+++ b/osu.Game/Database/ILive.cs
@@ -38,10 +38,10 @@ namespace osu.Game.Database
bool IsManaged { get; }
///
- /// Resolve the value of this instance on the current thread's context.
+ /// Resolve the value of this instance on the update thread.
///
///
- /// After resolving the data should not be passed between threads.
+ /// After resolving, the data should not be passed between threads.
///
T Value { get; }
}
diff --git a/osu.Game/Database/IModelFileManager.cs b/osu.Game/Database/IModelFileManager.cs
index 4bc1e2d29b..390be4a69d 100644
--- a/osu.Game/Database/IModelFileManager.cs
+++ b/osu.Game/Database/IModelFileManager.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Database
void DeleteFile(TModel model, TFileModel file);
///
- /// Add a new file.
+ /// Add a new file. If the file already exists, it is overwritten.
///
/// The item to operate on.
/// The new file contents.
diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs
index d8d2cb8981..7cd9ae2885 100644
--- a/osu.Game/Database/OsuDbContext.cs
+++ b/osu.Game/Database/OsuDbContext.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Database
public DbSet BeatmapSetInfo { get; set; }
public DbSet FileInfo { get; set; }
public DbSet RulesetInfo { get; set; }
- public DbSet SkinInfo { get; set; }
+ public DbSet SkinInfo { get; set; }
public DbSet ScoreInfo { get; set; }
// migrated to realm
@@ -133,8 +133,9 @@ namespace osu.Game.Database
modelBuilder.Entity().HasIndex(b => b.DeletePending);
modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique();
- modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique();
- modelBuilder.Entity().HasIndex(b => b.DeletePending);
+ modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique();
+ modelBuilder.Entity().HasIndex(b => b.DeletePending);
+ modelBuilder.Entity().HasMany(s => s.Files).WithOne(f => f.SkinInfo);
modelBuilder.Entity().HasIndex(b => new { b.RulesetID, b.Variant });
@@ -146,7 +147,7 @@ namespace osu.Game.Database
modelBuilder.Entity().HasOne(b => b.BaseDifficulty);
- modelBuilder.Entity().HasIndex(b => b.OnlineScoreID).IsUnique();
+ modelBuilder.Entity().HasIndex(b => b.OnlineID).IsUnique();
}
private class OsuDbLoggerFactory : ILoggerFactory
diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs
index 2bc77934a8..96c24837a1 100644
--- a/osu.Game/Database/RealmContextFactory.cs
+++ b/osu.Game/Database/RealmContextFactory.cs
@@ -14,6 +14,7 @@ using osu.Framework.Statistics;
using osu.Game.Configuration;
using osu.Game.Input.Bindings;
using osu.Game.Models;
+using osu.Game.Skinning;
using osu.Game.Stores;
using Realms;
@@ -52,6 +53,8 @@ namespace osu.Game.Database
///
private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1);
+ private readonly ThreadLocal currentThreadCanCreateContexts = new ThreadLocal();
+
private static readonly GlobalStatistic refreshes = GlobalStatistics.Get(@"Realm", @"Dirty Refreshes");
private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get(@"Realm", @"Contexts (Created)");
@@ -99,10 +102,6 @@ namespace osu.Game.Database
// This method triggers the first `CreateContext` call, which will implicitly run realm migrations and bring the schema up-to-date.
cleanupPendingDeletions();
-
- // Data migration is handled separately from schema migrations.
- // This is required as the user may be initialising realm for the first time ever, which would result in no schema migrations running.
- migrateDataFromEF();
}
private void cleanupPendingDeletions()
@@ -120,6 +119,11 @@ namespace osu.Game.Database
realm.Remove(s);
}
+ var pendingDeleteSkins = realm.All().Where(s => s.DeletePending);
+
+ foreach (var s in pendingDeleteSkins)
+ realm.Remove(s);
+
transaction.Commit();
}
@@ -151,9 +155,22 @@ namespace osu.Game.Database
if (isDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory));
+ bool tookSemaphoreLock = false;
+
try
{
- contextCreationLock.Wait();
+ if (!currentThreadCanCreateContexts.Value)
+ {
+ contextCreationLock.Wait();
+ currentThreadCanCreateContexts.Value = true;
+ tookSemaphoreLock = true;
+ }
+ else
+ {
+ // the semaphore is used to handle blocking of all context creation during certain periods.
+ // once the semaphore has been taken by this code section, it is safe to create further contexts on the same thread.
+ // this can happen if a realm subscription is active and triggers a callback which has user code that calls `CreateContext`.
+ }
contexts_created.Value++;
@@ -161,7 +178,11 @@ namespace osu.Game.Database
}
finally
{
- contextCreationLock.Release();
+ if (tookSemaphoreLock)
+ {
+ contextCreationLock.Release();
+ currentThreadCanCreateContexts.Value = false;
+ }
}
}
@@ -174,53 +195,6 @@ namespace osu.Game.Database
};
}
- private void migrateDataFromEF()
- {
- if (efContextFactory == null)
- return;
-
- using (var db = efContextFactory.GetForWrite())
- {
- // migrate ruleset settings. can be removed 20220315.
- var existingSettings = db.Context.DatabasedSetting;
-
- // previous entries in EF are removed post migration.
- if (!existingSettings.Any())
- return;
-
- using (var realm = CreateContext())
- using (var transaction = realm.BeginWrite())
- {
- // only migrate data if the realm database is empty.
- if (!realm.All().Any())
- {
- foreach (var dkb in existingSettings)
- {
- if (dkb.RulesetID == null)
- continue;
-
- string? shortName = getRulesetShortNameFromLegacyID(dkb.RulesetID.Value);
-
- if (string.IsNullOrEmpty(shortName))
- continue;
-
- realm.Add(new RealmRulesetSetting
- {
- Key = dkb.Key,
- Value = dkb.StringValue,
- RulesetName = shortName,
- Variant = dkb.Variant ?? 0,
- });
- }
- }
-
- db.Context.RemoveRange(existingSettings);
-
- transaction.Commit();
- }
- }
- }
-
private void onMigration(Migration migration, ulong lastSchemaVersion)
{
for (ulong i = lastSchemaVersion + 1; i <= schema_version; i++)
diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs
index 5ee40f5b4d..90b8814c24 100644
--- a/osu.Game/Database/RealmLive.cs
+++ b/osu.Game/Database/RealmLive.cs
@@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Threading;
+using osu.Framework.Development;
using Realms;
#nullable enable
@@ -19,27 +19,22 @@ namespace osu.Game.Database
public bool IsManaged => data.IsManaged;
- private readonly SynchronizationContext? fetchedContext;
- private readonly int fetchedThreadId;
-
///
/// The original live data used to create this instance.
///
private readonly T data;
+ private readonly RealmContextFactory realmFactory;
+
///
/// Construct a new instance of live realm data.
///
/// The realm data.
- public RealmLive(T data)
+ /// The realm factory the data was sourced from. May be null for an unmanaged object.
+ public RealmLive(T data, RealmContextFactory realmFactory)
{
this.data = data;
-
- if (data.IsManaged)
- {
- fetchedContext = SynchronizationContext.Current;
- fetchedThreadId = Thread.CurrentThread.ManagedThreadId;
- }
+ this.realmFactory = realmFactory;
ID = data.ID;
}
@@ -50,13 +45,13 @@ namespace osu.Game.Database
/// The action to perform.
public void PerformRead(Action perform)
{
- if (originalDataValid)
+ if (!IsManaged)
{
perform(data);
return;
}
- using (var realm = Realm.GetInstance(data.Realm.Config))
+ using (var realm = realmFactory.CreateContext())
perform(realm.Find(ID));
}
@@ -67,12 +62,12 @@ namespace osu.Game.Database
public TReturn PerformRead(Func perform)
{
if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn)))
- throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}.");
+ throw new InvalidOperationException(@$"Realm live objects should not exit the scope of {nameof(PerformRead)}.");
- if (originalDataValid)
+ if (!IsManaged)
return perform(data);
- using (var realm = Realm.GetInstance(data.Realm.Config))
+ using (var realm = realmFactory.CreateContext())
return perform(realm.Find(ID));
}
@@ -83,7 +78,7 @@ namespace osu.Game.Database
public void PerformWrite(Action perform)
{
if (!IsManaged)
- throw new InvalidOperationException("Can't perform writes on a non-managed underlying value");
+ throw new InvalidOperationException(@"Can't perform writes on a non-managed underlying value");
PerformRead(t =>
{
@@ -97,27 +92,18 @@ namespace osu.Game.Database
{
get
{
- if (originalDataValid)
+ if (!IsManaged)
return data;
- T retrieved;
+ if (!ThreadSafety.IsUpdateThread)
+ throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads");
- using (var realm = Realm.GetInstance(data.Realm.Config))
- retrieved = realm.Find(ID);
-
- if (!retrieved.IsValid)
- throw new InvalidOperationException("Attempted to access value without an open context");
-
- return retrieved;
+ return realmFactory.Context.Find(ID);
}
}
- private bool originalDataValid => !IsManaged || (isCorrectThread && data.IsValid);
-
- // this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72)
- private bool isCorrectThread
- => (fetchedContext != null && SynchronizationContext.Current == fetchedContext) || fetchedThreadId == Thread.CurrentThread.ManagedThreadId;
-
public bool Equals(ILive? other) => ID == other?.ID;
+
+ public override string ToString() => PerformRead(i => i.ToString());
}
}
diff --git a/osu.Game/Database/RealmLiveUnmanaged.cs b/osu.Game/Database/RealmLiveUnmanaged.cs
new file mode 100644
index 0000000000..ea50ccc1ff
--- /dev/null
+++ b/osu.Game/Database/RealmLiveUnmanaged.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Database
+{
+ ///
+ /// Provides a method of working with unmanaged realm objects.
+ /// Usually used for testing purposes where the instance is never required to be managed.
+ ///
+ /// The underlying object type.
+ public class RealmLiveUnmanaged : ILive where T : RealmObjectBase, IHasGuidPrimaryKey
+ {
+ ///
+ /// Construct a new instance of live realm data.
+ ///
+ /// The realm data.
+ public RealmLiveUnmanaged(T data)
+ {
+ Value = data;
+ }
+
+ public bool Equals(ILive? other) => ID == other?.ID;
+
+ public override string ToString() => Value.ToString();
+
+ public Guid ID => Value.ID;
+
+ public void PerformRead(Action perform) => perform(Value);
+
+ public TReturn PerformRead(Func perform) => perform(Value);
+
+ public void PerformWrite(Action perform) => throw new InvalidOperationException(@"Can't perform writes on a non-managed underlying value");
+
+ public bool IsManaged => false;
+
+ ///
+ /// The original live data used to create this instance.
+ ///
+ public T Value { get; }
+ }
+}
diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs
index ac4ca436ad..e5177823ba 100644
--- a/osu.Game/Database/RealmObjectExtensions.cs
+++ b/osu.Game/Database/RealmObjectExtensions.cs
@@ -1,12 +1,16 @@
// Copyright (c) ppy Pty Ltd . 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 AutoMapper;
+using osu.Framework.Development;
using osu.Game.Input.Bindings;
using Realms;
+#nullable enable
+
namespace osu.Game.Database
{
public static class RealmObjectExtensions
@@ -49,16 +53,132 @@ namespace osu.Game.Database
return mapper.Map(item);
}
- public static List> ToLive(this IEnumerable realmList)
+ public static List> ToLiveUnmanaged(this IEnumerable realmList)
where T : RealmObject, IHasGuidPrimaryKey
{
- return realmList.Select(l => new RealmLive(l)).Cast>().ToList();
+ return realmList.Select(l => new RealmLiveUnmanaged(l)).Cast>().ToList();
}
- public static ILive ToLive(this T realmObject)
+ public static ILive ToLiveUnmanaged(this T realmObject)
where T : RealmObject, IHasGuidPrimaryKey
{
- return new RealmLive(realmObject);
+ return new RealmLiveUnmanaged(realmObject);
+ }
+
+ public static List> ToLive(this IEnumerable realmList, RealmContextFactory realmContextFactory)
+ where T : RealmObject, IHasGuidPrimaryKey
+ {
+ return realmList.Select(l => new RealmLive(l, realmContextFactory)).Cast>().ToList();
+ }
+
+ public static ILive ToLive(this T realmObject, RealmContextFactory realmContextFactory)
+ where T : RealmObject, IHasGuidPrimaryKey
+ {
+ return new RealmLive(realmObject, realmContextFactory);
+ }
+
+ ///
+ /// Register a callback to be invoked each time this changes.
+ ///
+ ///
+ ///
+ /// This adds osu! specific thread and managed state safety checks on top of .
+ ///
+ ///
+ /// The first callback will be invoked with the initial after the asynchronous query completes,
+ /// and then called again after each write transaction which changes either any of the objects in the collection, or
+ /// which objects are in the collection. The changes parameter will
+ /// be null the first time the callback is invoked with the initial results. For each call after that,
+ /// it will contain information about which rows in the results were added, removed or modified.
+ ///
+ ///
+ /// If a write transaction did not modify any objects in this , the callback is not invoked at all.
+ /// If an error occurs the callback will be invoked with null for the sender parameter and a non-null error.
+ /// Currently the only errors that can occur are when opening the on the background worker thread.
+ ///
+ ///
+ /// At the time when the block is called, the object will be fully evaluated
+ /// and up-to-date, and as long as you do not perform a write transaction on the same thread
+ /// or explicitly call , accessing it will never perform blocking work.
+ ///
+ ///
+ /// Notifications are delivered via the standard event loop, and so can't be delivered while the event loop is blocked by other activity.
+ /// When notifications can't be delivered instantly, multiple notifications may be coalesced into a single notification.
+ /// This can include the notification with the initial collection.
+ ///
+ ///
+ /// The to observe for changes.
+ /// The callback to be invoked with the updated .
+ ///
+ /// A subscription token. It must be kept alive for as long as you want to receive change notifications.
+ /// To stop receiving notifications, call .
+ ///
+ /// May be null in the case the provided collection is not managed.
+ ///
+ ///
+ ///
+ public static IDisposable? QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback)
+ where T : RealmObjectBase
+ {
+ // Subscriptions can only work on the main thread.
+ if (!ThreadSafety.IsUpdateThread)
+ throw new InvalidOperationException("Cannot subscribe for realm notifications from a non-update thread.");
+
+ return collection.SubscribeForNotifications(callback);
+ }
+
+ ///
+ /// A convenience method that casts to and subscribes for change notifications.
+ ///
+ ///
+ /// This adds osu! specific thread and managed state safety checks on top of .
+ ///
+ /// The to observe for changes.
+ /// Type of the elements in the list.
+ ///
+ /// The callback to be invoked with the updated .
+ ///
+ /// A subscription token. It must be kept alive for as long as you want to receive change notifications.
+ /// To stop receiving notifications, call .
+ ///
+ /// May be null in the case the provided collection is not managed.
+ ///
+ public static IDisposable? QueryAsyncWithNotifications(this IQueryable list, NotificationCallbackDelegate callback)
+ where T : RealmObjectBase
+ {
+ // Subscribing to non-managed instances doesn't work.
+ // In this usage, the instance may be non-managed in tests.
+ if (!(list is IRealmCollection realmCollection))
+ return null;
+
+ return QueryAsyncWithNotifications(realmCollection, callback);
+ }
+
+ ///
+ /// A convenience method that casts to and subscribes for change notifications.
+ ///
+ ///
+ /// This adds osu! specific thread and managed state safety checks on top of .
+ ///
+ /// The to observe for changes.
+ /// Type of the elements in the list.
+ ///
+ /// The callback to be invoked with the updated .
+ ///
+ /// A subscription token. It must be kept alive for as long as you want to receive change notifications.
+ /// To stop receiving notifications, call .
+ ///
+ /// May be null in the case the provided collection is not managed.
+ ///
+ public static IDisposable? QueryAsyncWithNotifications(this IList list, NotificationCallbackDelegate callback)
+ where T : RealmObjectBase
+ {
+ // Subscribing to non-managed instances doesn't work.
+ // In this usage, the instance may be non-managed in tests.
+ if (!(list is IRealmCollection realmCollection))
+ return null;
+
+ return QueryAsyncWithNotifications(realmCollection, callback);
}
}
}
diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs
index dae2d2549c..26f4e9fb3b 100644
--- a/osu.Game/Database/UserLookupCache.cs
+++ b/osu.Game/Database/UserLookupCache.cs
@@ -100,6 +100,9 @@ namespace osu.Game.Database
}
}
+ if (userTasks.Count == 0)
+ return;
+
// Query the users.
var request = new GetUsersRequest(userTasks.Keys.ToArray());
diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs
index 2274da0fd4..f178a5c97b 100644
--- a/osu.Game/Extensions/ModelExtensions.cs
+++ b/osu.Game/Extensions/ModelExtensions.cs
@@ -104,6 +104,14 @@ namespace osu.Game.Extensions
/// Whether online IDs match. If either instance is missing an online ID, this will return false.
public static bool MatchesOnlineID(this APIUser? instance, APIUser? other) => matchesOnlineID(instance, other);
+ ///
+ /// Check whether the online ID of two s match.
+ ///
+ /// The instance to compare.
+ /// The other instance to compare against.
+ /// Whether online IDs match. If either instance is missing an online ID, this will return false.
+ public static bool MatchesOnlineID(this IScoreInfo? instance, IScoreInfo? other) => matchesOnlineID(instance, other);
+
private static bool matchesOnlineID(this IHasOnlineID? instance, IHasOnlineID? other)
{
if (instance == null || other == null)
diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
index 6e4901ab1a..2024d18570 100644
--- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
+++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
@@ -111,7 +111,7 @@ namespace osu.Game.Graphics.Containers
if (clock == null)
return;
- double currentTrackTime = clock.CurrentTime;
+ double currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds;
if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded)
{
@@ -132,13 +132,11 @@ namespace osu.Game.Graphics.Containers
{
// this may be the case where the beat syncing clock has been paused.
// we still want to show an idle animation, so use this container's time instead.
- currentTrackTime = Clock.CurrentTime;
+ currentTrackTime = Clock.CurrentTime + EarlyActivationMilliseconds;
timingPoint = TimingControlPoint.DEFAULT;
effectPoint = EffectControlPoint.DEFAULT;
}
- currentTrackTime += EarlyActivationMilliseconds;
-
double beatLength = timingPoint.BeatLength / Divisor;
while (beatLength < MinimumBeatLength)
diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs
index 3fa90e2330..8e272f637f 100644
--- a/osu.Game/Graphics/Cursor/MenuCursor.cs
+++ b/osu.Game/Graphics/Cursor/MenuCursor.cs
@@ -72,18 +72,21 @@ namespace osu.Game.Graphics.Cursor
protected override bool OnMouseDown(MouseDownEvent e)
{
- // only trigger animation for main mouse buttons
- activeCursor.Scale = new Vector2(1);
- activeCursor.ScaleTo(0.90f, 800, Easing.OutQuint);
-
- activeCursor.AdditiveLayer.Alpha = 0;
- activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint);
-
- if (cursorRotate.Value && dragRotationState != DragRotationState.Rotating)
+ if (State.Value == Visibility.Visible)
{
- // if cursor is already rotating don't reset its rotate origin
- dragRotationState = DragRotationState.DragStarted;
- positionMouseDown = e.MousePosition;
+ // only trigger animation for main mouse buttons
+ activeCursor.Scale = new Vector2(1);
+ activeCursor.ScaleTo(0.90f, 800, Easing.OutQuint);
+
+ activeCursor.AdditiveLayer.Alpha = 0;
+ activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint);
+
+ if (cursorRotate.Value && dragRotationState != DragRotationState.Rotating)
+ {
+ // if cursor is already rotating don't reset its rotate origin
+ dragRotationState = DragRotationState.DragStarted;
+ positionMouseDown = e.MousePosition;
+ }
}
return base.OnMouseDown(e);
diff --git a/osu.Game/Graphics/DateTooltip.cs b/osu.Game/Graphics/DateTooltip.cs
index 3094f9cc2b..d5768b259a 100644
--- a/osu.Game/Graphics/DateTooltip.cs
+++ b/osu.Game/Graphics/DateTooltip.cs
@@ -65,8 +65,10 @@ namespace osu.Game.Graphics
public void SetContent(DateTimeOffset date)
{
- dateText.Text = $"{date:d MMMM yyyy} ";
- timeText.Text = $"{date:HH:mm:ss \"UTC\"z}";
+ DateTimeOffset localDate = date.ToLocalTime();
+
+ dateText.Text = $"{localDate:d MMMM yyyy} ";
+ timeText.Text = $"{localDate:HH:mm:ss \"UTC\"z}";
}
public void Move(Vector2 pos) => Position = pos;
diff --git a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs
index 36288c745a..3d565a4464 100644
--- a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs
@@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Extensions;
+
namespace osu.Game.Graphics.UserInterface
{
public class OsuNumberBox : OsuTextBox
{
- protected override bool CanAddCharacter(char character) => char.IsNumber(character);
+ protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs
index 68ffc6bf4e..b7e25ae4e7 100644
--- a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.TopRight;
BackgroundColour = Color4.Black.Opacity(0.7f);
- MaxHeight = 400;
+ MaxHeight = 200;
}
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuTabDropdownMenuItem(item);
diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs
index 96319b9fdd..6db3068d84 100644
--- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs
@@ -93,7 +93,7 @@ namespace osu.Game.Graphics.UserInterface
if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples)
capsTextAddedSample?.Play();
else
- textAddedSamples[RNG.Next(0, 3)]?.Play();
+ playTextAddedSample();
}
protected override void OnUserTextRemoved(string removed)
@@ -117,6 +117,70 @@ namespace osu.Game.Graphics.UserInterface
caretMovedSample?.Play();
}
+ protected override void OnImeComposition(string newComposition, int removedTextLength, int addedTextLength, bool caretMoved)
+ {
+ base.OnImeComposition(newComposition, removedTextLength, addedTextLength, caretMoved);
+
+ if (string.IsNullOrEmpty(newComposition))
+ {
+ switch (removedTextLength)
+ {
+ case 0:
+ // empty composition event, composition wasn't changed, don't play anything.
+ return;
+
+ case 1:
+ // composition probably ended by pressing backspace, or was cancelled.
+ textRemovedSample?.Play();
+ return;
+
+ default:
+ // longer text removed, composition ended because it was cancelled.
+ // could be a different sample if desired.
+ textRemovedSample?.Play();
+ return;
+ }
+ }
+
+ if (addedTextLength > 0)
+ {
+ // some text was added, probably due to typing new text or by changing the candidate.
+ playTextAddedSample();
+ return;
+ }
+
+ if (removedTextLength > 0)
+ {
+ // text was probably removed by backspacing.
+ // it's also possible that a candidate that only removed text was changed to.
+ textRemovedSample?.Play();
+ return;
+ }
+
+ if (caretMoved)
+ {
+ // only the caret/selection was moved.
+ caretMovedSample?.Play();
+ }
+ }
+
+ protected override void OnImeResult(string result, bool successful)
+ {
+ base.OnImeResult(result, successful);
+
+ if (successful)
+ {
+ // composition was successfully completed, usually by pressing the enter key.
+ textCommittedSample?.Play();
+ }
+ else
+ {
+ // composition was prematurely ended, eg. by clicking inside the textbox.
+ // could be a different sample if desired.
+ textCommittedSample?.Play();
+ }
+ }
+
protected override void OnFocus(FocusEvent e)
{
BorderThickness = 3;
@@ -142,6 +206,8 @@ namespace osu.Game.Graphics.UserInterface
SelectionColour = SelectionColour,
};
+ private void playTextAddedSample() => textAddedSamples[RNG.Next(0, textAddedSamples.Length)]?.Play();
+
private class OsuCaret : Caret
{
private const float caret_move_time = 60;
diff --git a/osu.Game/IO/FileInfo.cs b/osu.Game/IO/FileInfo.cs
index 277ad0adac..148afba40d 100644
--- a/osu.Game/IO/FileInfo.cs
+++ b/osu.Game/IO/FileInfo.cs
@@ -9,6 +9,8 @@ namespace osu.Game.IO
{
public int ID { get; set; }
+ public bool IsManaged => ID > 0;
+
public string Hash { get; set; }
public int ReferenceCount { get; set; }
diff --git a/osu.Game/IO/IStorageResourceProvider.cs b/osu.Game/IO/IStorageResourceProvider.cs
index e4c97e18fa..950b5aae09 100644
--- a/osu.Game/IO/IStorageResourceProvider.cs
+++ b/osu.Game/IO/IStorageResourceProvider.cs
@@ -4,6 +4,7 @@
using osu.Framework.Audio;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
+using osu.Game.Database;
namespace osu.Game.IO
{
@@ -24,6 +25,11 @@ namespace osu.Game.IO
///
IResourceStore Resources { get; }
+ ///
+ /// Access realm.
+ ///
+ RealmContextFactory RealmContextFactory { get; }
+
///
/// Create a texture loader store based on an underlying data store.
///
diff --git a/osu.Game/IPC/ArchiveImportIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs
index d9d0e4c0ea..f381aad39a 100644
--- a/osu.Game/IPC/ArchiveImportIPCChannel.cs
+++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs
@@ -18,6 +18,7 @@ namespace osu.Game.IPC
: base(host)
{
this.importer = importer;
+
MessageReceived += msg =>
{
Debug.Assert(importer != null);
@@ -25,6 +26,8 @@ namespace osu.Game.IPC
{
if (t.Exception != null) throw t.Exception;
}, TaskContinuationOptions.OnlyOnFaulted);
+
+ return null;
};
}
diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
index baa5b9ff9c..f95c884fe5 100644
--- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
+++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
@@ -8,7 +8,6 @@ using osu.Framework.Allocation;
using osu.Framework.Input.Bindings;
using osu.Game.Database;
using osu.Game.Rulesets;
-using Realms;
namespace osu.Game.Input.Bindings
{
@@ -56,7 +55,7 @@ namespace osu.Game.Input.Bindings
.Where(b => b.RulesetName == rulesetName && b.Variant == variant);
realmSubscription = realmKeyBindings
- .SubscribeForNotifications((sender, changes, error) =>
+ .QueryAsyncWithNotifications((sender, changes, error) =>
{
// first subscription ignored as we are handling this in LoadComplete.
if (changes == null)
diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs
index 3bdb0a180d..cb51797685 100644
--- a/osu.Game/Input/RealmKeyBindingStore.cs
+++ b/osu.Game/Input/RealmKeyBindingStore.cs
@@ -81,20 +81,37 @@ namespace osu.Game.Input
// compare counts in database vs defaults for each action type.
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
{
- // avoid performing redundant queries when the database is empty and needs to be re-filled.
- int existingCount = existingBindings.Count(k => k.RulesetName == rulesetName && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key);
+ IEnumerable existing = existingBindings.Where(k =>
+ k.RulesetName == rulesetName
+ && k.Variant == variant
+ && k.ActionInt == (int)defaultsForAction.Key);
- if (defaultsForAction.Count() <= existingCount)
- continue;
+ int defaultsCount = defaultsForAction.Count();
+ int existingCount = existing.Count();
- // insert any defaults which are missing.
- realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding
+ if (defaultsCount > existingCount)
{
- KeyCombinationString = k.KeyCombination.ToString(),
- ActionInt = (int)k.Action,
- RulesetName = rulesetName,
- Variant = variant
- }));
+ // insert any defaults which are missing.
+ realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding
+ {
+ KeyCombinationString = k.KeyCombination.ToString(),
+ ActionInt = (int)k.Action,
+ RulesetName = rulesetName,
+ Variant = variant
+ }));
+ }
+ else if (defaultsCount < existingCount)
+ {
+ // generally this shouldn't happen, but if the user has more key bindings for an action than we expect,
+ // remove the last entries until the count matches for sanity.
+ foreach (var k in existing.TakeLast(existingCount - defaultsCount).ToArray())
+ {
+ realm.Remove(k);
+
+ // Remove from the local flattened/cached list so future lookups don't query now deleted rows.
+ existingBindings.Remove(k);
+ }
+ }
}
}
diff --git a/osu.Game/Localisation/MouseSettingsStrings.cs b/osu.Game/Localisation/MouseSettingsStrings.cs
index 5e894c4e0b..fd7225ad2e 100644
--- a/osu.Game/Localisation/MouseSettingsStrings.cs
+++ b/osu.Game/Localisation/MouseSettingsStrings.cs
@@ -35,9 +35,14 @@ namespace osu.Game.Localisation
public static LocalisableString ConfineMouseMode => new TranslatableString(getKey(@"confine_mouse_mode"), @"Confine mouse cursor to window");
///
- /// "Disable mouse wheel during gameplay"
+ /// "Disable mouse wheel adjusting volume during gameplay"
///
- public static LocalisableString DisableMouseWheel => new TranslatableString(getKey(@"disable_mouse_wheel"), @"Disable mouse wheel during gameplay");
+ public static LocalisableString DisableMouseWheelVolumeAdjust => new TranslatableString(getKey(@"disable_mouse_wheel_volume_adjust"), @"Disable mouse wheel adjusting volume during gameplay");
+
+ ///
+ /// "Volume can still be adjusted using the mouse wheel by holding "Alt""
+ ///
+ public static LocalisableString DisableMouseWheelVolumeAdjustTooltip => new TranslatableString(getKey(@"disable_mouse_wheel_volume_adjust_tooltip"), @"Volume can still be adjusted using the mouse wheel by holding ""Alt""");
///
/// "Disable mouse buttons during gameplay"
diff --git a/osu.Game/Models/RealmRuleset.cs b/osu.Game/Models/RealmRuleset.cs
index 9a7488fda2..b959d0b4dc 100644
--- a/osu.Game/Models/RealmRuleset.cs
+++ b/osu.Game/Models/RealmRuleset.cs
@@ -50,6 +50,8 @@ namespace osu.Game.Models
public bool Equals(RealmRuleset? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo;
+ public bool Equals(IRulesetInfo? other) => other is RealmRuleset b && Equals(b);
+
public override string ToString() => Name;
public RealmRuleset Clone() => new RealmRuleset
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index 43195811dc..91148c177f 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -38,7 +38,12 @@ namespace osu.Game.Online.API
protected override void PostProcess()
{
base.PostProcess();
- Response = ((OsuJsonWebRequest)WebRequest)?.ResponseObject;
+
+ if (WebRequest != null)
+ {
+ Response = ((OsuJsonWebRequest)WebRequest).ResponseObject;
+ Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network);
+ }
}
internal void TriggerSuccess(T result)
diff --git a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs
index 6cd45a41df..671f543422 100644
--- a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs
+++ b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs
@@ -26,9 +26,12 @@ namespace osu.Game.Online.API.Requests
{
var request = base.CreateWebRequest();
- request.AddParameter(@"id", beatmapInfo.OnlineID.ToString());
- request.AddParameter(@"checksum", beatmapInfo.MD5Hash);
- request.AddParameter(@"filename", filename);
+ if (beatmapInfo.OnlineID > 0)
+ request.AddParameter(@"id", beatmapInfo.OnlineID.ToString());
+ if (!string.IsNullOrEmpty(beatmapInfo.MD5Hash))
+ request.AddParameter(@"checksum", beatmapInfo.MD5Hash);
+ if (!string.IsNullOrEmpty(filename))
+ request.AddParameter(@"filename", filename);
return request;
}
diff --git a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs
new file mode 100644
index 0000000000..1d71e22b77
--- /dev/null
+++ b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class GetBeatmapsRequest : APIRequest
+ {
+ private readonly int[] beatmapIds;
+
+ private const int max_ids_per_request = 50;
+
+ public GetBeatmapsRequest(int[] beatmapIds)
+ {
+ if (beatmapIds.Length > max_ids_per_request)
+ throw new ArgumentException($"{nameof(GetBeatmapsRequest)} calls only support up to {max_ids_per_request} IDs at once");
+
+ this.beatmapIds = beatmapIds;
+ }
+
+ protected override string Target => "beatmaps/?ids[]=" + string.Join("&ids[]=", beatmapIds);
+ }
+}
diff --git a/osu.Game/Online/API/Requests/GetBeatmapsResponse.cs b/osu.Game/Online/API/Requests/GetBeatmapsResponse.cs
new file mode 100644
index 0000000000..c450c3269c
--- /dev/null
+++ b/osu.Game/Online/API/Requests/GetBeatmapsResponse.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class GetBeatmapsResponse : ResponseWithCursor
+ {
+ [JsonProperty("beatmaps")]
+ public List Beatmaps;
+ }
+}
diff --git a/osu.Game/Online/API/Requests/GetUserRequest.cs b/osu.Game/Online/API/Requests/GetUserRequest.cs
index e32451fc2f..28da5222f9 100644
--- a/osu.Game/Online/API/Requests/GetUserRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserRequest.cs
@@ -9,7 +9,7 @@ namespace osu.Game.Online.API.Requests
public class GetUserRequest : APIRequest
{
public readonly string Lookup;
- public readonly RulesetInfo Ruleset;
+ public readonly IRulesetInfo Ruleset;
private readonly LookupType lookupType;
///
@@ -24,7 +24,7 @@ namespace osu.Game.Online.API.Requests
///
/// The user to get.
/// The ruleset to get the user's info for.
- public GetUserRequest(long? userId = null, RulesetInfo ruleset = null)
+ public GetUserRequest(long? userId = null, IRulesetInfo ruleset = null)
{
Lookup = userId.ToString();
lookupType = LookupType.Id;
@@ -36,7 +36,7 @@ namespace osu.Game.Online.API.Requests
///
/// The user to get.
/// The ruleset to get the user's info for.
- public GetUserRequest(string username = null, RulesetInfo ruleset = null)
+ public GetUserRequest(string username = null, IRulesetInfo ruleset = null)
{
Lookup = username;
lookupType = LookupType.Username;
diff --git a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs
index e13ac8e539..653abf7427 100644
--- a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs
@@ -8,7 +8,7 @@ using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
- public class GetUserScoresRequest : PaginatedAPIRequest>
+ public class GetUserScoresRequest : PaginatedAPIRequest>
{
private readonly long userId;
private readonly ScoreType type;
diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APIScore.cs
similarity index 96%
rename from osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs
rename to osu.Game/Online/API/Requests/Responses/APIScore.cs
index 467d5a9f23..4f795bee6c 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIScore.cs
@@ -13,10 +13,11 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
+using osu.Game.Users;
namespace osu.Game.Online.API.Requests.Responses
{
- public class APIScoreInfo : IScoreInfo
+ public class APIScore : IScoreInfo
{
[JsonProperty(@"score")]
public long TotalScore { get; set; }
@@ -101,7 +102,7 @@ namespace osu.Game.Online.API.Requests.Responses
BeatmapInfo = beatmap,
User = User,
Accuracy = Accuracy,
- OnlineScoreID = OnlineID,
+ OnlineID = OnlineID,
Date = Date,
PP = PP,
RulesetID = RulesetID,
@@ -150,6 +151,11 @@ namespace osu.Game.Online.API.Requests.Responses
public IRulesetInfo Ruleset => new RulesetInfo { OnlineID = RulesetID };
IEnumerable IHasNamedFiles.Files => throw new NotImplementedException();
+ #region Implementation of IScoreInfo
+
IBeatmapInfo IScoreInfo.Beatmap => Beatmap;
+ IUser IScoreInfo.User => User;
+
+ #endregion
}
}
diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs b/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs
index 48b7134901..d3c9ba0c7e 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Online.API.Requests.Responses
public int? Position;
[JsonProperty(@"score")]
- public APIScoreInfo Score;
+ public APIScore Score;
public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null)
{
diff --git a/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs b/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
index 5304664bf8..283ebf2411 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
@@ -9,7 +9,7 @@ namespace osu.Game.Online.API.Requests.Responses
public class APIScoresCollection
{
[JsonProperty(@"scores")]
- public List Scores;
+ public List Scores;
[JsonProperty(@"userScore")]
public APIScoreWithPosition UserScore;
diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs
index ca6317566f..a11af7b305 100644
--- a/osu.Game/Online/Chat/MessageNotifier.cs
+++ b/osu.Game/Online/Chat/MessageNotifier.cs
@@ -1,10 +1,10 @@
// Copyright (c) ppy Pty Ltd . 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.Collections.Specialized;
using System.Linq;
+using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -120,16 +120,21 @@ namespace osu.Game.Online.Chat
private void checkForMentions(Channel channel, Message message)
{
- if (!notifyOnUsername.Value || !checkContainsUsername(message.Content, localUser.Value.Username)) return;
+ if (!notifyOnUsername.Value || !CheckContainsUsername(message.Content, localUser.Value.Username)) return;
notifications.Post(new MentionNotification(message.Sender.Username, channel));
}
///
- /// Checks if contains .
+ /// Checks if mentions .
/// This will match against the case where underscores are used instead of spaces (which is how osu-stable handles usernames with spaces).
///
- private static bool checkContainsUsername(string message, string username) => message.Contains(username, StringComparison.OrdinalIgnoreCase) || message.Contains(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase);
+ public static bool CheckContainsUsername(string message, string username)
+ {
+ string fullName = Regex.Escape(username);
+ string underscoreName = Regex.Escape(username.Replace(' ', '_'));
+ return Regex.IsMatch(message, $@"(^|\W)({fullName}|{underscoreName})($|\W)", RegexOptions.IgnoreCase);
+ }
public class PrivateMessageNotification : OpenChannelNotification
{
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
index e01c7c9e49..14eec8b388 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -65,9 +65,6 @@ namespace osu.Game.Online.Leaderboards
[Resolved(CanBeNull = true)]
private SongSelect songSelect { get; set; }
- [Resolved]
- private ScoreManager scoreManager { get; set; }
-
[Resolved]
private Storage storage { get; set; }
@@ -114,7 +111,7 @@ namespace osu.Game.Online.Leaderboards
background = new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = user.Id == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black,
+ Colour = user.OnlineID == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black,
Alpha = background_alpha,
},
},
diff --git a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs
index ab4210251e..3db497bd6a 100644
--- a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs
+++ b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs
@@ -3,13 +3,11 @@
using System;
using System.Threading;
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
-using osu.Game.Rulesets;
using osuTK;
namespace osu.Game.Online.Leaderboards
@@ -25,9 +23,6 @@ namespace osu.Game.Online.Leaderboards
protected override bool StartHidden => true;
- [Resolved]
- private RulesetStore rulesets { get; set; }
-
public UserTopScoreContainer(Func createScoreDelegate)
{
this.createScoreDelegate = createScoreDelegate;
diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs
index 3e84e4b904..073d512f90 100644
--- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs
+++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs
@@ -77,10 +77,27 @@ namespace osu.Game.Online.Multiplayer
/// If an attempt to start the game occurs when the game's (or users') state disallows it.
Task StartMatch();
+ ///
+ /// Aborts an ongoing gameplay load.
+ ///
+ Task AbortGameplay();
+
///
/// Adds an item to the playlist.
///
/// The item to add.
Task AddPlaylistItem(MultiplayerPlaylistItem item);
+
+ ///
+ /// Edits an existing playlist item with new values.
+ ///
+ /// The item to edit, containing new properties. Must have an ID.
+ Task EditPlaylistItem(MultiplayerPlaylistItem item);
+
+ ///
+ /// Removes an item from the playlist.
+ ///
+ /// The item to remove.
+ Task RemovePlaylistItem(long playlistItemId);
}
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 4c472164d6..903aaa89e3 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -32,12 +32,36 @@ namespace osu.Game.Online.Multiplayer
///
public event Action? RoomUpdated;
+ ///
+ /// Invoked when a new user joins the room.
+ ///
public event Action? UserJoined;
+ ///
+ /// Invoked when a user leaves the room of their own accord.
+ ///
public event Action? UserLeft;
+ ///
+ /// Invoked when a user was kicked from the room forcefully.
+ ///
public event Action? UserKicked;
+ ///
+ /// Invoked when a new item is added to the playlist.
+ ///
+ public event Action? ItemAdded;
+
+ ///
+ /// Invoked when a playlist item is removed from the playlist. The provided long is the playlist's item ID.
+ ///
+ public event Action? ItemRemoved;
+
+ ///
+ /// Invoked when a playlist item's details change.
+ ///
+ public event Action? ItemChanged;
+
///
/// Invoked when the multiplayer server requests the current beatmap to be loaded into play.
///
@@ -71,8 +95,6 @@ namespace osu.Game.Online.Multiplayer
protected readonly BindableList PlayingUserIds = new BindableList();
- public readonly Bindable CurrentMatchPlayingItem = new Bindable();
-
///
/// The corresponding to the local player, if available.
///
@@ -94,7 +116,7 @@ namespace osu.Game.Online.Multiplayer
protected IAPIProvider API { get; private set; } = null!;
[Resolved]
- protected RulesetStore Rulesets { get; private set; } = null!;
+ protected IRulesetStore Rulesets { get; private set; } = null!;
[Resolved]
private UserLookupCache userLookupCache { get; set; } = null!;
@@ -138,9 +160,6 @@ namespace osu.Game.Online.Multiplayer
var joinedRoom = await JoinRoom(room.RoomID.Value.Value, password ?? room.Password.Value).ConfigureAwait(false);
Debug.Assert(joinedRoom != null);
- // Populate playlist items.
- var playlistItems = await Task.WhenAll(joinedRoom.Playlist.Select(createPlaylistItem)).ConfigureAwait(false);
-
// Populate users.
Debug.Assert(joinedRoom.Users != null);
await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)).ConfigureAwait(false);
@@ -152,7 +171,7 @@ namespace osu.Game.Online.Multiplayer
APIRoom = room;
APIRoom.Playlist.Clear();
- APIRoom.Playlist.AddRange(playlistItems);
+ APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
Debug.Assert(LocalUser != null);
addUserToAPIRoom(LocalUser);
@@ -195,7 +214,6 @@ namespace osu.Game.Online.Multiplayer
{
APIRoom = null;
Room = null;
- CurrentMatchPlayingItem.Value = null;
PlayingUserIds.Clear();
RoomUpdated?.Invoke();
@@ -309,8 +327,14 @@ namespace osu.Game.Online.Multiplayer
public abstract Task StartMatch();
+ public abstract Task AbortGameplay();
+
public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item);
+ public abstract Task EditPlaylistItem(MultiplayerPlaylistItem item);
+
+ public abstract Task RemovePlaylistItem(long playlistItemId);
+
Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state)
{
if (Room == null)
@@ -446,7 +470,11 @@ namespace osu.Game.Online.Multiplayer
Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings)
{
+ Debug.Assert(APIRoom != null);
+ Debug.Assert(Room != null);
+
Scheduler.Add(() => updateLocalRoomSettings(newSettings));
+
return Task.CompletedTask;
}
@@ -600,12 +628,10 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask;
}
- public async Task PlaylistItemAdded(MultiplayerPlaylistItem item)
+ public Task PlaylistItemAdded(MultiplayerPlaylistItem item)
{
if (Room == null)
- return;
-
- var playlistItem = await createPlaylistItem(item).ConfigureAwait(false);
+ return Task.CompletedTask;
Scheduler.Add(() =>
{
@@ -615,10 +641,13 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(APIRoom != null);
Room.Playlist.Add(item);
- APIRoom.Playlist.Add(playlistItem);
+ APIRoom.Playlist.Add(createPlaylistItem(item));
+ ItemAdded?.Invoke(item);
RoomUpdated?.Invoke();
});
+
+ return Task.CompletedTask;
}
public Task PlaylistItemRemoved(long playlistItemId)
@@ -636,18 +665,17 @@ namespace osu.Game.Online.Multiplayer
Room.Playlist.Remove(Room.Playlist.Single(existing => existing.ID == playlistItemId));
APIRoom.Playlist.RemoveAll(existing => existing.ID == playlistItemId);
+ ItemRemoved?.Invoke(playlistItemId);
RoomUpdated?.Invoke();
});
return Task.CompletedTask;
}
- public async Task PlaylistItemChanged(MultiplayerPlaylistItem item)
+ public Task PlaylistItemChanged(MultiplayerPlaylistItem item)
{
if (Room == null)
- return;
-
- var playlistItem = await createPlaylistItem(item).ConfigureAwait(false);
+ return Task.CompletedTask;
Scheduler.Add(() =>
{
@@ -660,14 +688,13 @@ namespace osu.Game.Online.Multiplayer
int existingIndex = APIRoom.Playlist.IndexOf(APIRoom.Playlist.Single(existing => existing.ID == item.ID));
APIRoom.Playlist.RemoveAt(existingIndex);
- APIRoom.Playlist.Insert(existingIndex, playlistItem);
-
- // If the currently-selected item was the one that got replaced, update the selected item to the new one.
- if (CurrentMatchPlayingItem.Value?.ID == playlistItem.ID)
- CurrentMatchPlayingItem.Value = playlistItem;
+ APIRoom.Playlist.Insert(existingIndex, createPlaylistItem(item));
+ ItemChanged?.Invoke(item);
RoomUpdated?.Invoke();
});
+
+ return Task.CompletedTask;
}
///
@@ -694,34 +721,29 @@ namespace osu.Game.Online.Multiplayer
Room.Settings = settings;
APIRoom.Name.Value = Room.Settings.Name;
APIRoom.Password.Value = Room.Settings.Password;
+ APIRoom.Type.Value = Room.Settings.MatchType;
APIRoom.QueueMode.Value = Room.Settings.QueueMode;
- RoomUpdated?.Invoke();
- CurrentMatchPlayingItem.Value = APIRoom.Playlist.SingleOrDefault(p => p.ID == settings.PlaylistItemId);
+ RoomUpdated?.Invoke();
}
- private async Task createPlaylistItem(MultiplayerPlaylistItem item)
+ private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item)
{
- var set = await GetOnlineBeatmapSet(item.BeatmapID).ConfigureAwait(false);
-
- // The incoming response is deserialised without circular reference handling currently.
- // Because we require using metadata from this instance, populate the nested beatmaps' sets manually here.
- foreach (var b in set.Beatmaps)
- b.BeatmapSet = set;
-
- var beatmap = set.Beatmaps.Single(b => b.OnlineID == item.BeatmapID);
- beatmap.Checksum = item.BeatmapChecksum;
-
var ruleset = Rulesets.GetRuleset(item.RulesetID);
+
+ Debug.Assert(ruleset != null);
+
var rulesetInstance = ruleset.CreateInstance();
var playlistItem = new PlaylistItem
{
ID = item.ID,
+ BeatmapID = item.BeatmapID,
OwnerID = item.OwnerID,
- Beatmap = { Value = beatmap },
Ruleset = { Value = ruleset },
- Expired = item.Expired
+ Expired = item.Expired,
+ PlaylistOrder = item.PlaylistOrder,
+ PlayedAt = item.PlayedAt
};
playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance)));
@@ -731,12 +753,12 @@ namespace osu.Game.Online.Multiplayer
}
///
- /// Retrieves a from an online source.
+ /// Retrieves a from an online source.
///
- /// The beatmap set ID.
+ /// The beatmap ID.
/// A token to cancel the request.
- /// The retrieval task.
- protected abstract Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default);
+ /// The retrieval task.
+ public abstract Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default);
///
/// For the provided user ID, update whether the user is included in .
diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
index 7308c03ec3..3794bec228 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -9,8 +9,8 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Game.Database;
using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
@@ -29,6 +29,9 @@ namespace osu.Game.Online.Multiplayer
private HubConnection? connection => connector?.CurrentConnection;
+ [Resolved]
+ private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
+
public OnlineMultiplayerClient(EndpointConfiguration endpoints)
{
endpoint = endpoints.MultiplayerEndpointUrl;
@@ -151,6 +154,14 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch));
}
+ public override Task AbortGameplay()
+ {
+ if (!IsConnected.Value)
+ return Task.CompletedTask;
+
+ return connection.InvokeAsync(nameof(IMultiplayerServer.AbortGameplay));
+ }
+
public override Task AddPlaylistItem(MultiplayerPlaylistItem item)
{
if (!IsConnected.Value)
@@ -159,27 +170,25 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.AddPlaylistItem), item);
}
- protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default)
+ public override Task EditPlaylistItem(MultiplayerPlaylistItem item)
{
- var tcs = new TaskCompletionSource();
- var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId);
+ if (!IsConnected.Value)
+ return Task.CompletedTask;
- req.Success += res =>
- {
- if (cancellationToken.IsCancellationRequested)
- {
- tcs.SetCanceled();
- return;
- }
+ return connection.InvokeAsync(nameof(IMultiplayerServer.EditPlaylistItem), item);
+ }
- tcs.SetResult(res);
- };
+ public override Task RemovePlaylistItem(long playlistItemId)
+ {
+ if (!IsConnected.Value)
+ return Task.CompletedTask;
- req.Failure += e => tcs.SetException(e);
+ return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId);
+ }
- API.Queue(req);
-
- return tcs.Task;
+ public override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default)
+ {
+ return beatmapLookupCache.GetBeatmapAsync(beatmapId, cancellationToken);
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs
index 6ca0b822f3..8ec073ff1e 100644
--- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs
+++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs
@@ -39,6 +39,22 @@ namespace osu.Game.Online.Rooms
[Key(7)]
public bool Expired { get; set; }
+ ///
+ /// The order in which this will be played relative to others.
+ /// Playlist items should be played in increasing order (lower values are played first).
+ ///
+ ///
+ /// This is only valid for items which are not . The value for expired items is undefined and should not be used.
+ ///
+ [Key(8)]
+ public ushort PlaylistOrder { get; set; }
+
+ ///
+ /// The date when this was played.
+ ///
+ [Key(9)]
+ public DateTimeOffset? PlayedAt { get; set; }
+
public MultiplayerPlaylistItem()
{
}
@@ -46,12 +62,15 @@ namespace osu.Game.Online.Rooms
public MultiplayerPlaylistItem(PlaylistItem item)
{
ID = item.ID;
+ OwnerID = item.OwnerID;
BeatmapID = item.BeatmapID;
BeatmapChecksum = item.Beatmap.Value?.MD5Hash ?? string.Empty;
RulesetID = item.RulesetID;
RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray();
AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray();
Expired = item.Expired;
+ PlaylistOrder = item.PlaylistOrder ?? 0;
+ PlayedAt = item.PlayedAt;
}
}
}
diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs
index 7bc3377ad9..05c9a1b6cf 100644
--- a/osu.Game/Online/Rooms/MultiplayerScore.cs
+++ b/osu.Game/Online/Rooms/MultiplayerScore.cs
@@ -69,7 +69,7 @@ namespace osu.Game.Online.Rooms
var scoreInfo = new ScoreInfo
{
- OnlineScoreID = ID,
+ OnlineID = ID,
TotalScore = TotalScore,
MaxCombo = MaxCombo,
BeatmapInfo = beatmap,
diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
index aa0e37363b..a32f069470 100644
--- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
@@ -40,6 +40,11 @@ namespace osu.Game.Online.Rooms
private BeatmapDownloadTracker downloadTracker;
+ ///
+ /// The beatmap matching the required hash (and providing a final state).
+ ///
+ private BeatmapInfo matchingHash;
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -71,13 +76,34 @@ namespace osu.Game.Online.Rooms
progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500);
}, true);
}, true);
+
+ // These events are needed for a fringe case where a modified/altered beatmap is imported with matching OnlineIDs.
+ // During the import process this will cause the existing beatmap set to be silently deleted and replaced with the new one.
+ // This is not exposed to us via `BeatmapDownloadTracker` so we have to take it into our own hands (as we care about the hash matching).
+ beatmapManager.ItemUpdated += itemUpdated;
+ beatmapManager.ItemRemoved += itemRemoved;
}
+ private void itemUpdated(BeatmapSetInfo item) => Schedule(() =>
+ {
+ if (matchingHash?.BeatmapSet.ID == item.ID || SelectedItem.Value?.Beatmap.Value.BeatmapSet?.OnlineID == item.OnlineID)
+ updateAvailability();
+ });
+
+ private void itemRemoved(BeatmapSetInfo item) => Schedule(() =>
+ {
+ if (matchingHash?.BeatmapSet.ID == item.ID)
+ updateAvailability();
+ });
+
private void updateAvailability()
{
if (downloadTracker == null)
return;
+ // will be repopulated below if still valid.
+ matchingHash = null;
+
switch (downloadTracker.State.Value)
{
case DownloadState.NotDownloaded:
@@ -93,7 +119,9 @@ namespace osu.Game.Online.Rooms
break;
case DownloadState.LocallyAvailable:
- bool hashMatches = checkHashValidity();
+ matchingHash = findMatchingHash();
+
+ bool hashMatches = matchingHash != null;
availability.Value = hashMatches ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded();
@@ -108,12 +136,23 @@ namespace osu.Game.Online.Rooms
}
}
- private bool checkHashValidity()
+ private BeatmapInfo findMatchingHash()
{
int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID;
string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash;
- return beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId && b.MD5Hash == checksum && !b.BeatmapSet.DeletePending) != null;
+ return beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId && b.MD5Hash == checksum && !b.BeatmapSet.DeletePending);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (beatmapManager != null)
+ {
+ beatmapManager.ItemUpdated -= itemUpdated;
+ beatmapManager.ItemRemoved -= itemRemoved;
+ }
}
}
}
diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs
index a1480865b8..b8700fd067 100644
--- a/osu.Game/Online/Rooms/PlaylistItem.cs
+++ b/osu.Game/Online/Rooms/PlaylistItem.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Bindables;
@@ -33,6 +34,12 @@ namespace osu.Game.Online.Rooms
[JsonProperty("expired")]
public bool Expired { get; set; }
+ [JsonProperty("playlist_order")]
+ public ushort? PlaylistOrder { get; set; }
+
+ [JsonProperty("played_at")]
+ public DateTimeOffset? PlayedAt { get; set; }
+
[JsonIgnore]
public IBindable Valid => valid;
@@ -79,11 +86,13 @@ namespace osu.Game.Online.Rooms
public void MarkInvalid() => valid.Value = false;
- public void MapObjects(RulesetStore rulesets)
+ public void MapObjects(IRulesetStore rulesets)
{
Beatmap.Value ??= apiBeatmap;
Ruleset.Value ??= rulesets.GetRuleset(RulesetID);
+ Debug.Assert(Ruleset.Value != null);
+
Ruleset rulesetInstance = Ruleset.Value.CreateInstance();
if (allowedModsBacking != null)
diff --git a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs
index d5da6c401c..e24d113822 100644
--- a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs
+++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs
@@ -31,6 +31,7 @@ namespace osu.Game.Online.Rooms
req.ContentType = "application/json";
req.Method = HttpMethod.Put;
+ req.Timeout = 30000;
req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings
{
diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs
index e09cc7c9cd..68932cc388 100644
--- a/osu.Game/Online/ScoreDownloadTracker.cs
+++ b/osu.Game/Online/ScoreDownloadTracker.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
+using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Scoring;
@@ -35,7 +36,7 @@ namespace osu.Game.Online
var scoreInfo = new ScoreInfo
{
ID = TrackedItem.ID,
- OnlineScoreID = TrackedItem.OnlineScoreID
+ OnlineID = TrackedItem.OnlineID
};
if (Manager.IsAvailableLocally(scoreInfo))
@@ -113,7 +114,7 @@ namespace osu.Game.Online
UpdateState(DownloadState.NotDownloaded);
});
- private bool checkEquality(IScoreInfo x, IScoreInfo y) => x.OnlineID == y.OnlineID;
+ private bool checkEquality(IScoreInfo x, IScoreInfo y) => x.MatchesOnlineID(y);
#region Disposal
diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs
index 25c2e5a61f..99cf5ceff5 100644
--- a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs
+++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs
@@ -31,6 +31,7 @@ namespace osu.Game.Online.Solo
req.ContentType = "application/json";
req.Method = HttpMethod.Put;
+ req.Timeout = 30000;
req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings
{
diff --git a/osu.Game/Online/Solo/SubmittableScore.cs b/osu.Game/Online/Solo/SubmittableScore.cs
index 373c302844..5ca5ad9619 100644
--- a/osu.Game/Online/Solo/SubmittableScore.cs
+++ b/osu.Game/Online/Solo/SubmittableScore.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Online.Solo
{
///
/// A class specifically for sending scores to the API during score submission.
- /// This is used instead of due to marginally different serialisation naming requirements.
+ /// This is used instead of due to marginally different serialisation naming requirements.
///
[Serializable]
public class SubmittableScore
diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs
index 6b95d288c5..4da9bace70 100644
--- a/osu.Game/Online/Spectator/SpectatorClient.cs
+++ b/osu.Game/Online/Spectator/SpectatorClient.cs
@@ -136,30 +136,32 @@ namespace osu.Game.Online.Spectator
public void BeginPlaying(GameplayState state, Score score)
{
- Debug.Assert(ThreadSafety.IsUpdateThread);
+ // This schedule is only here to match the one below in `EndPlaying`.
+ Schedule(() =>
+ {
+ if (IsPlaying)
+ throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing");
- if (IsPlaying)
- throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing");
+ IsPlaying = true;
- IsPlaying = true;
+ // transfer state at point of beginning play
+ currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID;
+ currentState.RulesetID = score.ScoreInfo.RulesetID;
+ currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
- // transfer state at point of beginning play
- currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID;
- currentState.RulesetID = score.ScoreInfo.RulesetID;
- currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
+ currentBeatmap = state.Beatmap;
+ currentScore = score;
- currentBeatmap = state.Beatmap;
- currentScore = score;
-
- BeginPlayingInternal(currentState);
+ BeginPlayingInternal(currentState);
+ });
}
public void SendFrames(FrameDataBundle data) => lastSend = SendFramesInternal(data);
public void EndPlaying()
{
- // This method is most commonly called via Dispose(), which is asynchronous.
- // Todo: This should not be a thing, but requires framework changes.
+ // This method is most commonly called via Dispose(), which is can be asynchronous (via the AsyncDisposalQueue).
+ // We probably need to find a better way to handle this...
Schedule(() =>
{
if (!IsPlaying)
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 99b67976e3..a4471b56b9 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -103,7 +103,7 @@ namespace osu.Game
private Container topMostOverlayContent;
- private ScalingContainer screenContainer;
+ protected ScalingContainer ScreenContainer { get; private set; }
protected Container ScreenOffsetContainer { get; private set; }
@@ -161,7 +161,7 @@ namespace osu.Game
private Bindable uiScale;
- private Bindable configSkin;
+ private Bindable configSkin;
private readonly string[] args;
@@ -179,7 +179,7 @@ namespace osu.Game
}
private void updateBlockingOverlayFade() =>
- screenContainer.FadeColour(visibleBlockingOverlays.Any() ? OsuColour.Gray(0.5f) : Color4.White, 500, Easing.OutQuint);
+ ScreenContainer.FadeColour(visibleBlockingOverlays.Any() ? OsuColour.Gray(0.5f) : Color4.White, 500, Easing.OutQuint);
public void AddBlockingOverlay(OverlayContainer overlay)
{
@@ -243,27 +243,22 @@ namespace osu.Game
Ruleset.ValueChanged += r => configRuleset.Value = r.NewValue.ShortName;
// bind config int to database SkinInfo
- configSkin = LocalConfig.GetBindable(OsuSetting.Skin);
- SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID;
+ configSkin = LocalConfig.GetBindable(OsuSetting.Skin);
+ SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString();
configSkin.ValueChanged += skinId =>
{
- var skinInfo = SkinManager.Query(s => s.ID == skinId.NewValue);
+ ILive skinInfo = null;
+
+ if (Guid.TryParse(skinId.NewValue, out var guid))
+ skinInfo = SkinManager.Query(s => s.ID == guid);
if (skinInfo == null)
{
- switch (skinId.NewValue)
- {
- case -1:
- skinInfo = DefaultLegacySkin.Info;
- break;
-
- default:
- skinInfo = SkinInfo.Default;
- break;
- }
+ if (guid == SkinInfo.CLASSIC_SKIN)
+ skinInfo = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged();
}
- SkinManager.CurrentSkinInfo.Value = skinInfo;
+ SkinManager.CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.CreateInfo().ToLiveUnmanaged();
};
configSkin.TriggerChange();
@@ -492,8 +487,8 @@ namespace osu.Game
// to ensure all the required data for presenting a replay are present.
ScoreInfo databasedScoreInfo = null;
- if (score.OnlineScoreID != null)
- databasedScoreInfo = ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID);
+ if (score.OnlineID > 0)
+ databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID);
databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == score.Hash);
@@ -664,7 +659,7 @@ namespace osu.Game
// make config aware of how to lookup skins for on-screen display purposes.
// if this becomes a more common thing, tracked settings should be reconsidered to allow local DI.
- LocalConfig.LookupSkinName = id => SkinManager.GetAllUsableSkins().FirstOrDefault(s => s.ID == id)?.ToString() ?? "Unknown";
+ LocalConfig.LookupSkinName = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown";
LocalConfig.LookupKeyBindings = l =>
{
@@ -703,7 +698,7 @@ namespace osu.Game
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- screenContainer = new ScalingContainer(ScalingMode.ExcludeOverlays)
+ ScreenContainer = new ScalingContainer(ScalingMode.ExcludeOverlays)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
@@ -806,7 +801,7 @@ namespace osu.Game
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true);
loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true);
- loadComponentSingleFile(skinEditor = new SkinEditorOverlay(screenContainer), overlayContent.Add, true);
+ loadComponentSingleFile(skinEditor = new SkinEditorOverlay(ScreenContainer), overlayContent.Add, true);
loadComponentSingleFile(new LoginOverlay
{
@@ -1154,16 +1149,11 @@ namespace osu.Game
}
}
- private void screenPushed(IScreen lastScreen, IScreen newScreen)
- {
- ScreenChanged(lastScreen, newScreen);
- Logger.Log($"Screen changed → {newScreen}");
- }
+ private void screenPushed(IScreen lastScreen, IScreen newScreen) => ScreenChanged(lastScreen, newScreen);
private void screenExited(IScreen lastScreen, IScreen newScreen)
{
ScreenChanged(lastScreen, newScreen);
- Logger.Log($"Screen changed ← {newScreen}");
if (newScreen == null)
Exit();
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 88c9ab370c..2e266e32ff 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -142,6 +142,7 @@ namespace osu.Game
private BeatmapDifficultyCache difficultyCache;
private UserLookupCache userCache;
+ private BeatmapLookupCache beatmapCache;
private FileStore fileStore;
@@ -195,9 +196,12 @@ namespace osu.Game
runMigrations();
dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage));
+ dependencies.CacheAs(RulesetStore);
dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", contextFactory));
+ new EFToRealmMigrator(contextFactory, realmFactory, LocalConfig).Run();
+
dependencies.CacheAs(Storage);
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures")));
@@ -211,17 +215,9 @@ namespace osu.Game
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
- dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Resources, Audio));
+ dependencies.Cache(SkinManager = new SkinManager(Storage, realmFactory, Host, Resources, Audio, Scheduler));
dependencies.CacheAs(SkinManager);
- // needs to be done here rather than inside SkinManager to ensure thread safety of CurrentSkinInfo.
- SkinManager.ItemRemoved += item => Schedule(() =>
- {
- // check the removed skin is not the current user choice. if it is, switch back to default.
- if (item.Equals(SkinManager.CurrentSkinInfo.Value))
- SkinManager.CurrentSkinInfo.Value = SkinInfo.Default;
- });
-
EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl;
@@ -265,6 +261,9 @@ namespace osu.Game
dependencies.Cache(userCache = new UserLookupCache());
AddInternal(userCache);
+ dependencies.Cache(beatmapCache = new BeatmapLookupCache());
+ AddInternal(beatmapCache);
+
var scorePerformanceManager = new ScorePerformanceCache();
dependencies.Cache(scorePerformanceManager);
AddInternal(scorePerformanceManager);
@@ -377,6 +376,13 @@ namespace osu.Game
FrameStatistics.ValueChanged += e => fpsDisplayVisible.Value = e.NewValue != FrameStatisticsMode.None;
}
+ protected override void Update()
+ {
+ base.Update();
+
+ realmFactory.Refresh();
+ }
+
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@@ -442,10 +448,6 @@ namespace osu.Game
protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage);
- private void migrateDataToRealm()
- {
- }
-
private void onRulesetChanged(ValueChangedEvent r)
{
if (r.NewValue?.Available != true)
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
index 38f2bdb34f..f5b4785264 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
@@ -15,7 +15,6 @@ using osu.Framework.Threading;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Rulesets;
using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
@@ -61,9 +60,6 @@ namespace osu.Game.Overlays.BeatmapListing
[Resolved]
private IAPIProvider api { get; set; }
- [Resolved]
- private RulesetStore rulesets { get; set; }
-
public BeatmapListingFilterControl()
{
RelativeSizeAxes = Axes.X;
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index 49f2f5c211..6b27dbf847 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -151,7 +151,8 @@ namespace osu.Game.Overlays
}
// spawn new children with the contained so we only clear old content at the last moment.
- var content = new FillFlowContainer
+ // reverse ID flow is required for correct Z-ordering of the cards' expandable content (last card should be front-most).
+ var content = new ReverseChildIDFillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
diff --git a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs
index a9723c9c62..25aed4c980 100644
--- a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs
+++ b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Overlays.BeatmapSet
}
[Resolved]
- private RulesetStore rulesets { get; set; }
+ private IRulesetStore rulesets { get; set; }
private void onRulesetChanged(ValueChangedEvent ruleset)
{
@@ -57,8 +57,13 @@ namespace osu.Game.Overlays.BeatmapSet
if (ruleset.NewValue == null)
return;
+ var rulesetInstance = rulesets.GetRuleset(ruleset.NewValue.OnlineID)?.CreateInstance();
+
+ if (rulesetInstance == null)
+ return;
+
modsContainer.Add(new ModButton(new ModNoMod()));
- modsContainer.AddRange(rulesets.GetRuleset(ruleset.NewValue.OnlineID).CreateInstance().AllMods.Where(m => m.UserPlayable).Select(m => new ModButton(m)));
+ modsContainer.AddRange(rulesetInstance.AllMods.Where(m => m.UserPlayable).Select(m => new ModButton(m)));
modsContainer.ForEach(button =>
{
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
index 2fcdc9402d..695661d5c9 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
@@ -94,7 +94,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
topScoresContainer.Add(new DrawableTopScore(topScore));
- if (userScoreInfo != null && userScoreInfo.OnlineScoreID != topScore.OnlineScoreID)
+ if (userScoreInfo != null && userScoreInfo.OnlineID != topScore.OnlineID)
topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position));
}), TaskContinuationOptions.OnlyOnRanToCompletion);
});
diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs
index fa5a7c66d0..b9d3854066 100644
--- a/osu.Game/Overlays/BeatmapSetOverlay.cs
+++ b/osu.Game/Overlays/BeatmapSetOverlay.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -12,7 +11,6 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Overlays.Comments;
-using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
@@ -24,9 +22,6 @@ namespace osu.Game.Overlays
public const float Y_PADDING = 25;
public const float RIGHT_WIDTH = 275;
- [Resolved]
- private RulesetStore rulesets { get; set; }
-
private readonly Bindable beatmapSet = new Bindable();
// receive input outside our bounds so we can trigger a close event on ourselves.
diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs
index cc3ce63bf7..72473d5750 100644
--- a/osu.Game/Overlays/ChatOverlay.cs
+++ b/osu.Game/Overlays/ChatOverlay.cs
@@ -237,10 +237,7 @@ namespace osu.Game.Overlays
Schedule(() =>
{
// TODO: consider scheduling bindable callbacks to not perform when overlay is not present.
- channelManager.JoinedChannels.CollectionChanged += joinedChannelsChanged;
-
- foreach (Channel channel in channelManager.JoinedChannels)
- ChannelTabControl.AddChannel(channel);
+ channelManager.JoinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
channelManager.AvailableChannels.CollectionChanged += availableChannelsChanged;
availableChannelsChanged(null, null);
@@ -436,12 +433,19 @@ namespace osu.Game.Overlays
{
case NotifyCollectionChangedAction.Add:
foreach (Channel channel in args.NewItems.Cast())
- ChannelTabControl.AddChannel(channel);
+ {
+ if (channel.Type != ChannelType.Multiplayer)
+ ChannelTabControl.AddChannel(channel);
+ }
+
break;
case NotifyCollectionChangedAction.Remove:
foreach (Channel channel in args.OldItems.Cast())
{
+ if (!ChannelTabControl.Items.Contains(channel))
+ continue;
+
ChannelTabControl.RemoveChannel(channel);
var loaded = loadedChannels.Find(c => c.Channel == channel);
diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
index 269ed81bb5..0844975906 100644
--- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
+++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
@@ -43,9 +43,6 @@ namespace osu.Game.Overlays.Dashboard
};
}
- [Resolved]
- private IAPIProvider api { get; set; }
-
[Resolved]
private UserLookupCache users { get; set; }
diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs
index 51214fe460..9939ba024e 100644
--- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs
+++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs
@@ -5,12 +5,14 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Configuration;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
@@ -28,6 +30,8 @@ namespace osu.Game.Overlays.OSD
private Sample sampleOff;
private Sample sampleChange;
+ private Bindable lastPlaybackTime;
+
public TrackedSettingToast(SettingDescription description)
: base(description.Name, description.Value, description.Shortcut)
{
@@ -75,10 +79,28 @@ namespace osu.Game.Overlays.OSD
optionLights.Add(new OptionLight { Glowing = i == selectedOption });
}
+ [Resolved]
+ private SessionStatics statics { get; set; }
+
protected override void LoadComplete()
{
base.LoadComplete();
+ playSound();
+ }
+
+ private void playSound()
+ {
+ // This debounce code roughly follows what we're using in HoverSampleDebounceComponent.
+ // We're sharing the existing static for hover sounds because it doesn't really matter if they block each other.
+ // This is a simple solution, but if this ever becomes a problem (or other performance issues arise),
+ // the whole toast system should be rewritten to avoid recreating this drawable each time a value changes.
+ lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime);
+
+ bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME;
+
+ if (!enoughTimePassedSinceLastPlayback) return;
+
if (optionCount == 1)
{
if (selectedOption == 0)
@@ -93,6 +115,8 @@ namespace osu.Game.Overlays.OSD
sampleChange.Frequency.Value = 1 + (double)selectedOption / (optionCount - 1) * 0.25f;
sampleChange.Play();
}
+
+ lastPlaybackTime.Value = Time.Current;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Overlays/OnScreenDisplay.cs b/osu.Game/Overlays/OnScreenDisplay.cs
index af6d24fc65..6b3696ced9 100644
--- a/osu.Game/Overlays/OnScreenDisplay.cs
+++ b/osu.Game/Overlays/OnScreenDisplay.cs
@@ -95,13 +95,13 @@ namespace osu.Game.Overlays
/// Displays the provided temporarily.
///
///
- public void Display(Toast toast)
+ public void Display(Toast toast) => Schedule(() =>
{
box.Child = toast;
DisplayTemporarily(box);
- }
+ });
- private void displayTrackedSettingChange(SettingDescription description) => Schedule(() => Display(new TrackedSettingToast(description)));
+ private void displayTrackedSettingChange(SettingDescription description) => Scheduler.AddOnce(Display, new TrackedSettingToast(description));
private TransformSequence fadeIn;
private ScheduledDelegate fadeOut;
diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
index d6e515d8a1..d195babcbf 100644
--- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
@@ -42,7 +42,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void updateStatistics(UserStatistics statistics)
{
- int[] userRanks = statistics?.RankHistory?.Data;
+ // checking both IsRanked and RankHistory is required.
+ // see https://github.com/ppy/osu-web/blob/154ceafba0f35a1dd935df53ec98ae2ea5615f9f/resources/assets/lib/profile-page/rank-chart.tsx#L46
+ int[] userRanks = statistics?.IsRanked == true ? statistics.RankHistory?.Data : null;
Data = userRanks?.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray();
}
diff --git a/osu.Game/Overlays/Profile/ProfileSection.cs b/osu.Game/Overlays/Profile/ProfileSection.cs
index 6223b32814..fc6fce0d8e 100644
--- a/osu.Game/Overlays/Profile/ProfileSection.cs
+++ b/osu.Game/Overlays/Profile/ProfileSection.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
@@ -22,7 +23,7 @@ namespace osu.Game.Overlays.Profile
public abstract string Identifier { get; }
- private readonly FillFlowContainer content;
+ private readonly FillFlowContainer content;
private readonly Box background;
private readonly Box underscore;
@@ -79,7 +80,9 @@ namespace osu.Game.Overlays.Profile
}
}
},
- content = new FillFlowContainer
+ // reverse ID flow is required for correct Z-ordering of the content (last item should be front-most).
+ // particularly important in BeatmapsSection, as it uses beatmap cards, which have expandable overhanging content.
+ content = new ReverseChildIDFillFlowContainer
{
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,
diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs
index affe9ecb0c..9dcbf6142d 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs
+++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs
@@ -1,21 +1,21 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osuTK;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Online.API;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Rulesets;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics;
using osu.Framework.Localisation;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
+using osuTK;
namespace osu.Game.Overlays.Profile.Sections
{
@@ -24,13 +24,10 @@ namespace osu.Game.Overlays.Profile.Sections
[Resolved]
private IAPIProvider api { get; set; }
- [Resolved]
- protected RulesetStore Rulesets { get; private set; }
-
protected int VisiblePages;
protected int ItemsPerPage;
- protected FillFlowContainer ItemsContainer { get; private set; }
+ protected ReverseChildIDFillFlowContainer ItemsContainer { get; private set; }
private APIRequest> retrievalRequest;
private CancellationTokenSource loadCancellation;
@@ -52,11 +49,15 @@ namespace osu.Game.Overlays.Profile.Sections
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
- ItemsContainer = new FillFlowContainer
+ // reverse ID flow is required for correct Z-ordering of the items (last item should be front-most).
+ // particularly important in PaginatedBeatmapContainer, as it uses beatmap cards, which have expandable overhanging content.
+ ItemsContainer = new ReverseChildIDFillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(0, 2),
+ // ensure the container and its contents are in front of the "more" button.
+ Depth = float.MinValue
},
moreButton = new ShowMoreButton
{
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
index fb464e1b41..562be0403e 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
private const float performance_background_shear = 0.45f;
- protected readonly APIScoreInfo Score;
+ protected readonly APIScore Score;
[Resolved]
private OsuColour colours { get; set; }
@@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
- public DrawableProfileScore(APIScoreInfo score)
+ public DrawableProfileScore(APIScore score)
{
Score = score;
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs
index e653be5cfa..78ae0a5634 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
{
private readonly double weight;
- public DrawableProfileWeightedScore(APIScoreInfo score, double weight)
+ public DrawableProfileWeightedScore(APIScore score, double weight)
: base(score)
{
this.weight = weight;
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
index c3f10587a9..5532e35cc5 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
@@ -15,7 +15,7 @@ using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Overlays.Profile.Sections.Ranks
{
- public class PaginatedScoreContainer : PaginatedProfileSubsection
+ public class PaginatedScoreContainer : PaginatedProfileSubsection
{
private readonly ScoreType type;
@@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
}
}
- protected override void OnItemsReceived(List items)
+ protected override void OnItemsReceived(List items)
{
if (VisiblePages == 0)
drawableItemIndex = 0;
@@ -59,12 +59,12 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
base.OnItemsReceived(items);
}
- protected override APIRequest> CreateRequest() =>
+ protected override APIRequest> CreateRequest() =>
new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage);
private int drawableItemIndex;
- protected override Drawable CreateDrawableItem(APIScoreInfo model)
+ protected override Drawable CreateDrawableItem(APIScore model)
{
switch (type)
{
diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs
index cb8dae0bbc..7a27c6e4e1 100644
--- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs
+++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent
private IAPIProvider api { get; set; }
[Resolved]
- private RulesetStore rulesets { get; set; }
+ private IRulesetStore rulesets { get; set; }
private readonly APIRecentActivity activity;
diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
index cc553ad361..bcfc2499b9 100644
--- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
+++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
@@ -1,21 +1,21 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Graphics;
-using osu.Framework.Bindables;
-using osu.Game.Rulesets;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Online.API.Requests.Responses;
-using osuTK;
-using osu.Framework.Allocation;
-using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
-using osu.Game.Overlays.Rankings.Tables;
using System.Linq;
using System.Threading;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays.Rankings.Tables;
+using osu.Game.Rulesets;
+using osuTK;
namespace osu.Game.Overlays.Rankings
{
@@ -29,9 +29,6 @@ namespace osu.Game.Overlays.Rankings
[Resolved]
private IAPIProvider api { get; set; }
- [Resolved]
- private RulesetStore rulesets { get; set; }
-
private CancellationTokenSource cancellationToken;
private GetSpotlightRankingsRequest getRankingsRequest;
private GetSpotlightsRequest spotlightsRequest;
@@ -138,7 +135,8 @@ namespace osu.Game.Overlays.Rankings
Children = new Drawable[]
{
new ScoresTable(1, response.Users),
- new FillFlowContainer
+ // reverse ID flow is required for correct Z-ordering of the cards' expandable content (last card should be front-most).
+ new ReverseChildIDFillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
index 0334167759..4235dc0a05 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
@@ -67,7 +67,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
},
new SettingsCheckbox
{
- LabelText = MouseSettingsStrings.DisableMouseWheel,
+ LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust,
+ TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip,
Current = osuConfig.GetBindable(OsuSetting.MouseDisableWheel)
},
new SettingsCheckbox
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
index acdf9cdea6..98ccbf85fd 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
@@ -106,7 +106,10 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
dialogOverlay?.Push(new MassDeleteConfirmationDialog(() =>
{
deleteSkinsButton.Enabled.Value = false;
- Task.Run(() => skins.Delete(skins.GetAllUserSkins())).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true));
+ Task.Run(() =>
+ {
+ skins.Delete();
+ }).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true));
}));
}
});
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index 0eb65b4b0f..0fa6d78d4b 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -32,32 +32,26 @@ namespace osu.Game.Overlays.Settings.Sections
Icon = FontAwesome.Solid.PaintBrush
};
- private readonly Bindable dropdownBindable = new Bindable { Default = SkinInfo.Default };
- private readonly Bindable configBindable = new Bindable();
+ private readonly Bindable> dropdownBindable = new Bindable> { Default = DefaultSkin.CreateInfo().ToLiveUnmanaged() };
+ private readonly Bindable configBindable = new Bindable();
- private static readonly SkinInfo random_skin_info = new SkinInfo
+ private static readonly ILive random_skin_info = new SkinInfo
{
ID = SkinInfo.RANDOM_SKIN,
Name = "",
- };
+ }.ToLiveUnmanaged();
- private List skinItems;
-
- private int firstNonDefaultSkinIndex
- {
- get
- {
- int index = skinItems.FindIndex(s => s.ID > 0);
- if (index < 0)
- index = skinItems.Count;
-
- return index;
- }
- }
+ private List> skinItems;
[Resolved]
private SkinManager skins { get; set; }
+ [Resolved]
+ private RealmContextFactory realmFactory { get; set; }
+
+ private IDisposable realmSubscription;
+ private IQueryable realmSkins;
+
[BackgroundDependencyLoader(permitNulls: true)]
private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor)
{
@@ -75,96 +69,95 @@ namespace osu.Game.Overlays.Settings.Sections
new ExportSkinButton(),
};
- skins.ItemUpdated += itemUpdated;
- skins.ItemRemoved += itemRemoved;
-
config.BindWith(OsuSetting.Skin, configBindable);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
skinDropdown.Current = dropdownBindable;
+
+ realmSkins = realmFactory.Context.All()
+ .Where(s => !s.DeletePending)
+ .OrderByDescending(s => s.Protected) // protected skins should be at the top.
+ .ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase);
+
+ realmSubscription = realmSkins
+ .QueryAsyncWithNotifications((sender, changes, error) =>
+ {
+ if (changes == null)
+ return;
+
+ // Eventually this should be handling the individual changes rather than refreshing the whole dropdown.
+ updateItems();
+ });
+
updateItems();
- // Todo: This should not be necessary when OsuConfigManager is databased
- if (skinDropdown.Items.All(s => s.ID != configBindable.Value))
- configBindable.Value = 0;
+ configBindable.BindValueChanged(id => Scheduler.AddOnce(updateSelectedSkinFromConfig));
+ updateSelectedSkinFromConfig();
- configBindable.BindValueChanged(id => Scheduler.AddOnce(updateSelectedSkinFromConfig), true);
dropdownBindable.BindValueChanged(skin =>
{
- if (skin.NewValue == random_skin_info)
+ if (skin.NewValue.Equals(random_skin_info))
{
+ var skinBefore = skins.CurrentSkinInfo.Value;
+
skins.SelectRandomSkin();
+
+ if (skinBefore == skins.CurrentSkinInfo.Value)
+ {
+ // the random selection didn't change the skin, so we should manually update the dropdown to match.
+ dropdownBindable.Value = skins.CurrentSkinInfo.Value;
+ }
+
return;
}
- configBindable.Value = skin.NewValue.ID;
+ configBindable.Value = skin.NewValue.ID.ToString();
});
}
private void updateSelectedSkinFromConfig()
{
- int id = configBindable.Value;
+ ILive skin = null;
- var skin = skinDropdown.Items.FirstOrDefault(s => s.ID == id);
+ if (Guid.TryParse(configBindable.Value, out var configId))
+ skin = skinDropdown.Items.FirstOrDefault(s => s.ID == configId);
- if (skin == null)
- {
- // there may be a thread race condition where an item is selected that hasn't yet been added to the dropdown.
- // to avoid adding complexity, let's just ensure the item is added so we can perform the selection.
- skin = skins.Query(s => s.ID == id);
- addItem(skin);
- }
-
- dropdownBindable.Value = skin;
+ dropdownBindable.Value = skin ?? skinDropdown.Items.First();
}
private void updateItems()
{
- skinItems = skins.GetAllUsableSkins();
- skinItems.Insert(firstNonDefaultSkinIndex, random_skin_info);
- sortUserSkins(skinItems);
+ int protectedCount = realmSkins.Count(s => s.Protected);
+
+ skinItems = realmSkins.ToLive(realmFactory);
+
+ skinItems.Insert(protectedCount, random_skin_info);
+
skinDropdown.Items = skinItems;
}
- private void itemUpdated(SkinInfo item) => Schedule(() => addItem(item));
-
- private void addItem(SkinInfo item)
- {
- List newDropdownItems = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToList();
- sortUserSkins(newDropdownItems);
- skinDropdown.Items = newDropdownItems;
- }
-
- private void itemRemoved(SkinInfo item) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => !i.Equals(item)).ToArray());
-
- private void sortUserSkins(List skinsList)
- {
- // Sort user skins separately from built-in skins
- skinsList.Sort(firstNonDefaultSkinIndex, skinsList.Count - firstNonDefaultSkinIndex,
- Comparer.Create((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)));
- }
-
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
- if (skins != null)
- {
- skins.ItemUpdated -= itemUpdated;
- skins.ItemRemoved -= itemRemoved;
- }
+ realmSubscription?.Dispose();
}
- private class SkinSettingsDropdown : SettingsDropdown
+ private class SkinSettingsDropdown : SettingsDropdown>
{
- protected override OsuDropdown CreateDropdown() => new SkinDropdownControl();
+ protected override OsuDropdown> CreateDropdown() => new SkinDropdownControl();
private class SkinDropdownControl : DropdownControl
{
- protected override LocalisableString GenerateItemText(SkinInfo item) => item.ToString();
+ protected override LocalisableString GenerateItemText(ILive item) => item.ToString();
}
}
- private class ExportSkinButton : SettingsButton
+ public class ExportSkinButton : SettingsButton
{
[Resolved]
private SkinManager skins { get; set; }
@@ -179,16 +172,21 @@ namespace osu.Game.Overlays.Settings.Sections
{
Text = SkinSettingsStrings.ExportSkinButton;
Action = export;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
currentSkin = skins.CurrentSkin.GetBoundCopy();
- currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.ID > 0, true);
+ currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => !s.Protected), true);
}
private void export()
{
try
{
- new LegacySkinExporter(storage).Export(currentSkin.Value.SkinInfo);
+ currentSkin.Value.SkinInfo.PerformRead(s => new LegacySkinExporter(storage).Export(s));
}
catch (Exception e)
{
diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs
index cbe9f7fc64..cc4446033a 100644
--- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs
+++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
@@ -67,7 +68,7 @@ namespace osu.Game.Overlays.Settings
private class OutlinedNumberBox : OutlinedTextBox
{
- protected override bool CanAddCharacter(char character) => char.IsNumber(character);
+ protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
public new void NotifyInputError() => base.NotifyInputError();
}
diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs
index 6f0b433acb..789ed457a4 100644
--- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs
+++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs
@@ -5,19 +5,14 @@ using System.Linq;
using Markdig.Extensions.Yaml;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
-using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Game.Graphics.Containers.Markdown;
-using osu.Game.Online.API;
namespace osu.Game.Overlays.Wiki.Markdown
{
public class WikiMarkdownContainer : OsuMarkdownContainer
{
- [Resolved]
- private IAPIProvider api { get; set; }
-
public string CurrentPath
{
set => DocumentUrl = value;
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
index 01b4150030..6b61dd3efb 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
@@ -120,14 +120,14 @@ namespace osu.Game.Rulesets.Difficulty
/// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
///
/// A collection of structures describing the difficulty of the beatmap for each mod combination.
- public IEnumerable CalculateAll()
+ public IEnumerable CalculateAll(CancellationToken cancellationToken = default)
{
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
{
if (combination is MultiMod multi)
- yield return Calculate(multi.Mods);
+ yield return Calculate(multi.Mods, cancellationToken);
else
- yield return Calculate(combination.Yield());
+ yield return Calculate(combination.Yield(), cancellationToken);
}
}
@@ -145,7 +145,11 @@ namespace osu.Game.Rulesets.Difficulty
{
playableMods = mods.Select(m => m.DeepClone()).ToArray();
- Beatmap = beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
+ // Only pass through the cancellation token if it's non-default.
+ // This allows for the default timeout to be applied for playable beatmap construction.
+ Beatmap = cancellationToken == default
+ ? beatmap.GetPlayableBeatmap(ruleset, playableMods)
+ : beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
var track = new TrackVirtual(10000);
playableMods.OfType().ForEach(m => m.ApplyToTrack(track));
diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs
index 4e529a73fb..6599e0d59d 100644
--- a/osu.Game/Rulesets/IRulesetInfo.cs
+++ b/osu.Game/Rulesets/IRulesetInfo.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Game.Database;
#nullable enable
@@ -10,7 +11,7 @@ namespace osu.Game.Rulesets
///
/// A representation of a ruleset's metadata.
///
- public interface IRulesetInfo : IHasOnlineID
+ public interface IRulesetInfo : IHasOnlineID, IEquatable
{
///
/// The user-exposed name of this ruleset.
diff --git a/osu.Game/Rulesets/IRulesetStore.cs b/osu.Game/Rulesets/IRulesetStore.cs
new file mode 100644
index 0000000000..08d907810b
--- /dev/null
+++ b/osu.Game/Rulesets/IRulesetStore.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+
+#nullable enable
+
+namespace osu.Game.Rulesets
+{
+ public interface IRulesetStore
+ {
+ ///
+ /// Retrieve a ruleset using a known ID.
+ ///
+ /// The ruleset's internal ID.
+ /// A ruleset, if available, else null.
+ IRulesetInfo? GetRuleset(int id);
+
+ ///
+ /// Retrieve a ruleset using a known short name.
+ ///
+ /// The ruleset's short name.
+ /// A ruleset, if available, else null.
+ IRulesetInfo? GetRuleset(string shortName);
+
+ ///
+ /// All available rulesets.
+ ///
+ IEnumerable AvailableRulesets { get; }
+ }
+}
diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs
index c78088ba2d..f28ef1edeb 100644
--- a/osu.Game/Rulesets/Mods/ModCinema.cs
+++ b/osu.Game/Rulesets/Mods/ModCinema.cs
@@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Mods
drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
// AlwaysPresent required for hitsounds
- drawableRuleset.Playfield.AlwaysPresent = true;
- drawableRuleset.Playfield.Hide();
+ drawableRuleset.AlwaysPresent = true;
+ drawableRuleset.Hide();
}
}
diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs
index 4a146c05bf..d018cc4194 100644
--- a/osu.Game/Rulesets/RulesetInfo.cs
+++ b/osu.Game/Rulesets/RulesetInfo.cs
@@ -49,6 +49,8 @@ namespace osu.Game.Rulesets
public override bool Equals(object obj) => obj is RulesetInfo rulesetInfo && Equals(rulesetInfo);
+ public bool Equals(IRulesetInfo other) => other is RulesetInfo b && Equals(b);
+
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
public override int GetHashCode()
{
diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index 6dd036c0e6..5cc6a75f43 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -13,7 +13,7 @@ using osu.Game.Database;
namespace osu.Game.Rulesets
{
- public class RulesetStore : DatabaseBackedStore, IDisposable
+ public class RulesetStore : DatabaseBackedStore, IRulesetStore, IDisposable
{
private const string ruleset_library_prefix = "osu.Game.Rulesets";
@@ -236,5 +236,13 @@ namespace osu.Game.Rulesets
{
AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly;
}
+
+ #region Implementation of IRulesetStore
+
+ IRulesetInfo IRulesetStore.GetRuleset(int id) => GetRuleset(id);
+ IRulesetInfo IRulesetStore.GetRuleset(string shortName) => GetRuleset(shortName);
+ IEnumerable IRulesetStore.AvailableRulesets => AvailableRulesets;
+
+ #endregion
}
}
diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index 52aecb27de..d0bbf859af 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -19,7 +19,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Skinning;
using osuTK;
using System.Diagnostics;
-using osu.Framework.Audio.Sample;
namespace osu.Game.Rulesets.UI
{
@@ -88,9 +87,6 @@ namespace osu.Game.Rulesets.UI
[Resolved(CanBeNull = true)]
private IReadOnlyList mods { get; set; }
- [Resolved]
- private ISampleStore sampleStore { get; set; }
-
///
/// Creates a new .
///
diff --git a/osu.Game/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs
index 8b5b228632..b4ad183cd3 100644
--- a/osu.Game/Scoring/IScoreInfo.cs
+++ b/osu.Game/Scoring/IScoreInfo.cs
@@ -4,14 +4,14 @@
using System;
using osu.Game.Beatmaps;
using osu.Game.Database;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
+using osu.Game.Users;
namespace osu.Game.Scoring
{
public interface IScoreInfo : IHasOnlineID, IHasNamedFiles
{
- APIUser User { get; }
+ IUser User { get; }
long TotalScore { get; }
diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
index f943422389..fefee370b9 100644
--- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
+++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
@@ -80,12 +80,9 @@ namespace osu.Game.Scoring.Legacy
byte[] compressedReplay = sr.ReadByteArray();
if (version >= 20140721)
- scoreInfo.OnlineScoreID = sr.ReadInt64();
+ scoreInfo.OnlineID = sr.ReadInt64();
else if (version >= 20121008)
- scoreInfo.OnlineScoreID = sr.ReadInt32();
-
- if (scoreInfo.OnlineScoreID <= 0)
- scoreInfo.OnlineScoreID = null;
+ scoreInfo.OnlineID = sr.ReadInt32();
if (compressedReplay?.Length > 0)
{
diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
index 7b8cacb35b..3d67aa9558 100644
--- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
+++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Scoring.Legacy
sw.Write(LATEST_VERSION);
sw.Write(score.ScoreInfo.BeatmapInfo.MD5Hash);
sw.Write(score.ScoreInfo.UserString);
- sw.Write($"lazer-{score.ScoreInfo.UserString}-{score.ScoreInfo.Date}".ComputeMD5Hash());
+ sw.Write(FormattableString.Invariant($"lazer-{score.ScoreInfo.UserString}-{score.ScoreInfo.Date}").ComputeMD5Hash());
sw.Write((ushort)(score.ScoreInfo.GetCount300() ?? 0));
sw.Write((ushort)(score.ScoreInfo.GetCount100() ?? 0));
sw.Write((ushort)(score.ScoreInfo.GetCount50() ?? 0));
@@ -110,7 +110,9 @@ namespace osu.Game.Scoring.Legacy
}
}
- replayData.AppendFormat(@"{0}|{1}|{2}|{3},", -12345, 0, 0, 0);
+ // Warning: this is purposefully hardcoded as a string rather than interpolating, as in some cultures the minus sign is not encoded as the standard ASCII U+00C2 codepoint,
+ // which then would break decoding.
+ replayData.Append(@"-12345|0|0|0");
return replayData.ToString();
}
}
diff --git a/osu.Game/Scoring/ScoreFileInfo.cs b/osu.Game/Scoring/ScoreFileInfo.cs
index b2e81d4b8d..4c88cfa021 100644
--- a/osu.Game/Scoring/ScoreFileInfo.cs
+++ b/osu.Game/Scoring/ScoreFileInfo.cs
@@ -11,6 +11,8 @@ namespace osu.Game.Scoring
{
public int ID { get; set; }
+ public bool IsManaged => ID > 0;
+
public int FileInfoID { get; set; }
public FileInfo FileInfo { get; set; }
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index 564aa3b98c..7acc7bd055 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -14,6 +14,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Users;
using osu.Game.Utils;
namespace osu.Game.Scoring
@@ -22,6 +23,8 @@ namespace osu.Game.Scoring
{
public int ID { get; set; }
+ public bool IsManaged => ID > 0;
+
public ScoreRank Rank { get; set; }
public long TotalScore { get; set; }
@@ -134,7 +137,14 @@ namespace osu.Game.Scoring
[Column("Beatmap")]
public BeatmapInfo BeatmapInfo { get; set; }
- public long? OnlineScoreID { get; set; }
+ private long? onlineID;
+
+ [Column("OnlineScoreID")]
+ public long? OnlineID
+ {
+ get => onlineID;
+ set => onlineID = value > 0 ? value : null;
+ }
public DateTimeOffset Date { get; set; }
@@ -229,24 +239,18 @@ namespace osu.Game.Scoring
public bool Equals(ScoreInfo other)
{
- if (other == null)
- return false;
+ if (ReferenceEquals(this, other)) return true;
+ if (other == null) return false;
if (ID != 0 && other.ID != 0)
return ID == other.ID;
- if (OnlineScoreID.HasValue && other.OnlineScoreID.HasValue)
- return OnlineScoreID == other.OnlineScoreID;
-
- if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash))
- return Hash == other.Hash;
-
- return ReferenceEquals(this, other);
+ return false;
}
#region Implementation of IHasOnlineID
- public long OnlineID => OnlineScoreID ?? -1;
+ long IHasOnlineID.OnlineID => OnlineID ?? -1;
#endregion
@@ -254,6 +258,7 @@ namespace osu.Game.Scoring
IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo;
IRulesetInfo IScoreInfo.Ruleset => Ruleset;
+ IUser IScoreInfo.User => User;
bool IScoreInfo.HasReplay => Files.Any();
#endregion
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index e9cd44ae83..6de6b57066 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -71,7 +71,7 @@ namespace osu.Game.Scoring
return scores.Select((score, index) => (score, totalScore: totalScores[index]))
.OrderByDescending(g => g.totalScore)
- .ThenBy(g => g.score.OnlineScoreID)
+ .ThenBy(g => g.score.OnlineID)
.Select(g => g.score)
.ToArray();
}
diff --git a/osu.Game/Scoring/ScoreModelDownloader.cs b/osu.Game/Scoring/ScoreModelDownloader.cs
index 038a4bc351..514b7a57de 100644
--- a/osu.Game/Scoring/ScoreModelDownloader.cs
+++ b/osu.Game/Scoring/ScoreModelDownloader.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Database;
+using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
@@ -17,6 +18,6 @@ namespace osu.Game.Scoring
protected override ArchiveDownloadRequest CreateDownloadRequest(IScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score);
public override ArchiveDownloadRequest GetExistingDownload(IScoreInfo model)
- => CurrentDownloads.Find(r => r.Model.OnlineID == model.OnlineID);
+ => CurrentDownloads.Find(r => r.Model.MatchesOnlineID(model));
}
}
diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs
index 2cbd3aded7..44f0fe4fdf 100644
--- a/osu.Game/Scoring/ScoreModelManager.cs
+++ b/osu.Game/Scoring/ScoreModelManager.cs
@@ -66,6 +66,6 @@ namespace osu.Game.Scoring
protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable items)
=> base.CheckLocalAvailability(model, items)
- || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID));
+ || (model.OnlineID > 0 && items.Any(i => i.OnlineID == model.OnlineID));
}
}
diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
index 4a922c45b9..452f033dcc 100644
--- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
+++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
@@ -48,16 +48,19 @@ namespace osu.Game.Screens.Backgrounds
AddInternal(seasonalBackgroundLoader);
- user.ValueChanged += _ => Next();
- skin.ValueChanged += _ => Next();
- mode.ValueChanged += _ => Next();
- beatmap.ValueChanged += _ => Next();
- introSequence.ValueChanged += _ => Next();
- seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Next();
+ user.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
+ skin.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
+ mode.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
+ beatmap.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
+ introSequence.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
+ seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Scheduler.AddOnce(loadNextIfRequired);
currentDisplay = RNG.Next(0, background_count);
Next();
+
+ // helper function required for AddOnce usage.
+ void loadNextIfRequired() => Next();
}
private ScheduledDelegate nextTask;
@@ -67,7 +70,7 @@ namespace osu.Game.Screens.Backgrounds
/// Request loading the next background.
///
/// Whether a new background was queued for load. May return false if the current background is still valid.
- public bool Next()
+ public virtual bool Next()
{
var nextBackground = createBackground();
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs
index 4629f9b540..f0e643f805 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs
@@ -1,20 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
public class GroupVisualisation : CompositeDrawable
{
- [Resolved]
- private OsuColour colours { get; set; }
-
public readonly ControlPointGroup Group;
private readonly IBindableList controlPoints = new BindableList();
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index b8fa05e7eb..265f56534f 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -279,9 +279,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
editorClock.Start();
}
- [Resolved]
- private EditorBeatmap beatmap { get; set; }
-
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; }
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs
index 2b2e66fb18..9610f6424c 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs
@@ -1,12 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
@@ -16,9 +14,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly IBindableList controlPoints = new BindableList();
- [Resolved]
- private OsuColour colours { get; set; }
-
public TimelineControlPointGroup(ControlPointGroup group)
{
Group = group;
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
index 80aa6972b1..1839b0507d 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
@@ -184,9 +184,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private SamplePointPiece sampleOverrideDisplay;
private DifficultyPointPiece difficultyOverrideDisplay;
- [Resolved]
- private EditorBeatmap beatmap { get; set; }
-
private DifficultyControlPoint difficultyControlPoint;
private SampleControlPoint sampleControlPoint;
diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index 3b02d42b41..9386538a78 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -83,7 +83,9 @@ namespace osu.Game.Screens.Edit.Compose
{
base.LoadComplete();
EditorBeatmap.SelectedHitObjects.BindCollectionChanged((_, __) => updateClipboardActionAvailability());
- clipboard.BindValueChanged(_ => updateClipboardActionAvailability(), true);
+ clipboard.BindValueChanged(_ => updateClipboardActionAvailability());
+ composer.OnLoadComplete += _ => updateClipboardActionAvailability();
+ updateClipboardActionAvailability();
}
#region Clipboard operations
@@ -131,7 +133,7 @@ namespace osu.Game.Screens.Edit.Compose
private void updateClipboardActionAvailability()
{
CanCut.Value = CanCopy.Value = EditorBeatmap.SelectedHitObjects.Any();
- CanPaste.Value = !string.IsNullOrEmpty(clipboard.Value);
+ CanPaste.Value = composer.IsLoaded && !string.IsNullOrEmpty(clipboard.Value);
}
private string formatSelectionAsString()
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index ac71298f36..48489c60ab 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -109,9 +109,6 @@ namespace osu.Game.Screens.Edit
[Resolved]
private IAPIProvider api { get; set; }
- [Resolved]
- private MusicController music { get; set; }
-
[Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard();
diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs
index 2a01a5b6b2..15d70e28b6 100644
--- a/osu.Game/Screens/Edit/EditorLoader.cs
+++ b/osu.Game/Screens/Edit/EditorLoader.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -9,6 +10,7 @@ using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
@@ -53,6 +55,14 @@ namespace osu.Game.Screens.Edit
});
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // will be restored via lease, see `DisallowExternalBeatmapRulesetChanges`.
+ Mods.Value = Array.Empty();
+ }
+
protected virtual Editor CreateEditor() => new Editor(this);
protected override void LogoArriving(OsuLogo logo, bool resuming)
diff --git a/osu.Game/Screens/Edit/EditorRoundedScreen.cs b/osu.Game/Screens/Edit/EditorRoundedScreen.cs
index 7f7b3abc2a..62f40f0325 100644
--- a/osu.Game/Screens/Edit/EditorRoundedScreen.cs
+++ b/osu.Game/Screens/Edit/EditorRoundedScreen.cs
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
using osu.Game.Overlays;
namespace osu.Game.Screens.Edit
@@ -14,9 +13,6 @@ namespace osu.Game.Screens.Edit
{
public const int HORIZONTAL_PADDING = 100;
- [Resolved]
- private OsuColour colours { get; set; }
-
private Container roundedContent;
protected override Container Content => roundedContent;
diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs
index ab8bd6a3bc..a67a060134 100644
--- a/osu.Game/Screens/Edit/EditorTable.cs
+++ b/osu.Game/Screens/Edit/EditorTable.cs
@@ -62,9 +62,6 @@ namespace osu.Game.Screens.Edit
private readonly Box hoveredBackground;
- [Resolved]
- private EditorClock clock { get; set; }
-
public RowBackground(object item)
{
Item = item;
diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs
index f833bc49f7..d1e35ae20d 100644
--- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs
+++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs
@@ -15,7 +15,6 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Database;
-using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK;
@@ -36,9 +35,6 @@ namespace osu.Game.Screens.Edit.Setup
[Resolved]
private OsuGameBase game { get; set; }
- [Resolved]
- private SectionsContainer sectionsContainer { get; set; }
-
public FileChooserLabelledTextBox(params string[] handledExtensions)
{
this.handledExtensions = handledExtensions;
diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs
index 7a98cf63c3..1e6899e05f 100644
--- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs
+++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs
@@ -132,9 +132,6 @@ namespace osu.Game.Screens.Edit.Timing
controlPoints.BindTo(group.ControlPoints);
}
- [Resolved]
- private OsuColour colours { get; set; }
-
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs
index fd238feeac..cadcdebc6e 100644
--- a/osu.Game/Screens/Edit/Verify/IssueList.cs
+++ b/osu.Game/Screens/Edit/Verify/IssueList.cs
@@ -23,9 +23,6 @@ namespace osu.Game.Screens.Edit.Verify
{
private IssueTable table;
- [Resolved]
- private EditorClock clock { get; set; }
-
[Resolved]
private IBindable workingBeatmap { get; set; }
diff --git a/osu.Game/Screens/Menu/StorageErrorDialog.cs b/osu.Game/Screens/Menu/StorageErrorDialog.cs
index dcaad4013a..250623ec68 100644
--- a/osu.Game/Screens/Menu/StorageErrorDialog.cs
+++ b/osu.Game/Screens/Menu/StorageErrorDialog.cs
@@ -15,9 +15,6 @@ namespace osu.Game.Screens.Menu
[Resolved]
private DialogOverlay dialogOverlay { get; set; }
- [Resolved]
- private OsuGameBase osuGame { get; set; }
-
public StorageErrorDialog(OsuStorage storage, OsuStorageError error)
{
HeaderText = "osu! storage error";
diff --git a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs
index b013cbafd8..89842e933b 100644
--- a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms;
+using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Select;
using osuTK;
@@ -43,9 +44,9 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = 10 },
- Child = playlist = new DrawableRoomPlaylist(true, false)
+ Child = playlist = new PlaylistsRoomSettingsPlaylist
{
- RelativeSizeAxes = Axes.Both,
+ RelativeSizeAxes = Axes.Both
}
}
},
diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
index ddfdab18f7..238aa4059d 100644
--- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
@@ -7,9 +7,9 @@ using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Logging;
-using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@@ -28,10 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
private readonly Bindable joinedRoom = new Bindable();
[Resolved]
- private RulesetStore rulesets { get; set; }
-
- [Resolved]
- private BeatmapManager beatmaps { get; set; }
+ private IRulesetStore rulesets { get; set; }
[Resolved]
private IAPIProvider api { get; set; }
@@ -111,6 +108,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
public void AddOrUpdateRoom(Room room)
{
+ Debug.Assert(ThreadSafety.IsUpdateThread);
Debug.Assert(room.RoomID.Value != null);
if (ignoredRooms.Contains(room.RoomID.Value.Value))
@@ -140,12 +138,16 @@ namespace osu.Game.Screens.OnlinePlay.Components
public void RemoveRoom(Room room)
{
+ Debug.Assert(ThreadSafety.IsUpdateThread);
+
rooms.Remove(room);
notifyRoomsUpdated();
}
public void ClearRooms()
{
+ Debug.Assert(ThreadSafety.IsUpdateThread);
+
rooms.Clear();
notifyRoomsUpdated();
}
diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs
index b9d2bdf23e..22842fbb9e 100644
--- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
-using osu.Framework.Allocation;
using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Components
@@ -12,9 +11,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
///
public class SelectionPollingComponent : RoomPollingComponent
{
- [Resolved]
- private IRoomManager roomManager { get; set; }
-
private readonly Room room;
public SelectionPollingComponent(Room room)
diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs
index fc029543bb..edf9c5d155 100644
--- a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs
@@ -80,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
private void updateRange(object sender, NotifyCollectionChangedEventArgs e)
{
- var orderedDifficulties = Playlist.Select(p => p.Beatmap.Value).OrderBy(b => b.StarRating).ToArray();
+ var orderedDifficulties = Playlist.Where(p => p.Beatmap.Value != null).Select(p => p.Beatmap.Value).OrderBy(b => b.StarRating).ToArray();
StarDifficulty minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarRating : 0, 0);
StarDifficulty maxDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[^1].StarRating : 0, 0);
diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs
index 6deca0482a..57bb4253cb 100644
--- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs
+++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs
@@ -1,11 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Collections.Generic;
-using System.Collections.Specialized;
+using System;
using System.Linq;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
@@ -14,38 +12,136 @@ using osuTK;
namespace osu.Game.Screens.OnlinePlay
{
+ ///
+ /// A scrollable list which displays the s in a .
+ ///
public class DrawableRoomPlaylist : OsuRearrangeableListContainer
{
+ ///
+ /// The currently-selected item. Selection is visually represented with a border.
+ /// May be updated by clicking playlist items if is true.
+ ///
public readonly Bindable SelectedItem = new Bindable();
- private readonly bool allowEdit;
- private readonly bool allowSelection;
- private readonly bool showItemOwner;
+ ///
+ /// Invoked when an item is requested to be deleted.
+ ///
+ public Action RequestDeletion;
- public DrawableRoomPlaylist(bool allowEdit, bool allowSelection, bool reverse = false, bool showItemOwner = false)
+ ///
+ /// Invoked when an item requests its results to be shown.
+ ///
+ public Action RequestResults;
+
+ ///
+ /// Invoked when an item requests to be edited.
+ ///
+ public Action RequestEdit;
+
+ private bool allowReordering;
+
+ ///
+ /// Whether to allow reordering items in the playlist.
+ ///
+ public bool AllowReordering
{
- this.allowEdit = allowEdit;
- this.allowSelection = allowSelection;
- this.showItemOwner = showItemOwner;
+ get => allowReordering;
+ set
+ {
+ allowReordering = value;
- ((ReversibleFillFlowContainer)ListContainer).Reverse = reverse;
+ foreach (var item in ListContainer.OfType())
+ item.AllowReordering = value;
+ }
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
+ private bool allowDeletion;
- // Scheduled since items are removed and re-added upon rearrangement
- Items.CollectionChanged += (_, args) => Schedule(() =>
+ ///
+ /// Whether to allow deleting items from the playlist.
+ /// If true, requests to delete items may be satisfied via .
+ ///
+ public bool AllowDeletion
+ {
+ get => allowDeletion;
+ set
{
- switch (args.Action)
- {
- case NotifyCollectionChangedAction.Remove:
- if (allowSelection && args.OldItems.Contains(SelectedItem))
- SelectedItem.Value = null;
- break;
- }
- });
+ allowDeletion = value;
+
+ foreach (var item in ListContainer.OfType())
+ item.AllowDeletion = value;
+ }
+ }
+
+ private bool allowSelection;
+
+ ///
+ /// Whether to allow selecting items from the playlist.
+ /// If true, clicking on items in the playlist will change the value of .
+ ///
+ public bool AllowSelection
+ {
+ get => allowSelection;
+ set
+ {
+ allowSelection = value;
+
+ foreach (var item in ListContainer.OfType())
+ item.AllowSelection = value;
+ }
+ }
+
+ private bool allowShowingResults;
+
+ ///
+ /// Whether to allow items to request their results to be shown.
+ /// If true, requests to show the results may be satisfied via .
+ ///
+ public bool AllowShowingResults
+ {
+ get => allowShowingResults;
+ set
+ {
+ allowShowingResults = value;
+
+ foreach (var item in ListContainer.OfType())
+ item.AllowShowingResults = value;
+ }
+ }
+
+ private bool allowEditing;
+
+ ///
+ /// Whether to allow items to be edited.
+ /// If true, requests to edit items may be satisfied via .
+ ///
+ public bool AllowEditing
+ {
+ get => allowEditing;
+ set
+ {
+ allowEditing = value;
+
+ foreach (var item in ListContainer.OfType())
+ item.AllowEditing = value;
+ }
+ }
+
+ private bool showItemOwners;
+
+ ///
+ /// Whether to show the avatar of users which own each playlist item.
+ ///
+ public bool ShowItemOwners
+ {
+ get => showItemOwners;
+ set
+ {
+ showItemOwners = value;
+
+ foreach (var item in ListContainer.OfType())
+ item.ShowItemOwner = value;
+ }
}
protected override ScrollContainer CreateScrollContainer() => base.CreateScrollContainer().With(d =>
@@ -53,45 +149,25 @@ namespace osu.Game.Screens.OnlinePlay
d.ScrollbarVisible = false;
});
- protected override FillFlowContainer> CreateListFillFlowContainer() => new ReversibleFillFlowContainer
+ protected override FillFlowContainer> CreateListFillFlowContainer() => new FillFlowContainer>
{
Spacing = new Vector2(0, 2)
};
- protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => new DrawableRoomPlaylistItem(item, allowEdit, allowSelection, showItemOwner)
+ protected sealed override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => CreateDrawablePlaylistItem(item).With(d =>
{
- SelectedItem = { BindTarget = SelectedItem },
- RequestDeletion = requestDeletion
- };
+ d.SelectedItem.BindTarget = SelectedItem;
+ d.RequestDeletion = i => RequestDeletion?.Invoke(i);
+ d.RequestResults = i => RequestResults?.Invoke(i);
+ d.RequestEdit = i => RequestEdit?.Invoke(i);
+ d.AllowReordering = AllowReordering;
+ d.AllowDeletion = AllowDeletion;
+ d.AllowSelection = AllowSelection;
+ d.AllowShowingResults = AllowShowingResults;
+ d.AllowEditing = AllowEditing;
+ d.ShowItemOwner = ShowItemOwners;
+ });
- private void requestDeletion(PlaylistItem item)
- {
- if (allowSelection && SelectedItem.Value == item)
- {
- if (Items.Count == 1)
- SelectedItem.Value = null;
- else
- SelectedItem.Value = Items.GetNext(item) ?? Items[^2];
- }
-
- Items.Remove(item);
- }
-
- private class ReversibleFillFlowContainer : FillFlowContainer>
- {
- private bool reverse;
-
- public bool Reverse
- {
- get => reverse;
- set
- {
- reverse = value;
- Invalidate();
- }
- }
-
- public override IEnumerable FlowingChildren => Reverse ? base.FlowingChildren.OrderBy(d => -GetLayoutPosition(d)) : base.FlowingChildren;
- }
+ protected virtual DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new DrawableRoomPlaylistItem(item);
}
}
diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs
index 2dbe2df82c..e1f7ea5e92 100644
--- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs
+++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs
@@ -5,9 +5,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -16,6 +16,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
+using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
@@ -24,6 +25,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.Chat;
+using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Rulesets;
@@ -38,27 +40,51 @@ namespace osu.Game.Screens.OnlinePlay
public class DrawableRoomPlaylistItem : OsuRearrangeableListItem
{
public const float HEIGHT = 50;
- public const float ICON_HEIGHT = 34;
+ private const float icon_height = 34;
+
+ ///
+ /// Invoked when this item requests to be deleted.
+ ///
public Action RequestDeletion;
+ ///
+ /// Invoked when this item requests its results to be shown.
+ ///
+ public Action RequestResults;
+
+ ///
+ /// Invoked when this item requests to be edited.
+ ///
+ public Action RequestEdit;
+
+ ///
+ /// The currently-selected item, used to show a border around this item.
+ /// May be updated by this item if is true.
+ ///
public readonly Bindable SelectedItem = new Bindable();
+ public readonly PlaylistItem Item;
+
+ private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both };
+ private readonly IBindable valid = new Bindable();
+ private readonly Bindable beatmap = new Bindable();
+ private readonly Bindable ruleset = new Bindable();
+ private readonly BindableList requiredMods = new BindableList();
+
private Container maskingContainer;
private Container difficultyIconContainer;
private LinkFlowContainer beatmapText;
private LinkFlowContainer authorText;
private ExplicitContentBeatmapPill explicitContentPill;
private ModDisplay modDisplay;
+ private FillFlowContainer buttonsFlow;
private UpdateableAvatar ownerAvatar;
-
- private readonly IBindable valid = new Bindable();
-
- private readonly Bindable beatmap = new Bindable();
- private readonly Bindable ruleset = new Bindable();
- private readonly BindableList requiredMods = new BindableList();
-
- public readonly PlaylistItem Item;
+ private Drawable showResultsButton;
+ private Drawable editButton;
+ private Drawable removeButton;
+ private PanelBackground panelBackground;
+ private FillFlowContainer mainFillFlow;
[Resolved]
private OsuColour colours { get; set; }
@@ -66,29 +92,25 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved]
private UserLookupCache userLookupCache { get; set; }
- private readonly bool allowEdit;
- private readonly bool allowSelection;
- private readonly bool showItemOwner;
+ [CanBeNull]
+ [Resolved(CanBeNull = true)]
+ private MultiplayerClient multiplayerClient { get; set; }
- protected override bool ShouldBeConsideredForInput(Drawable child) => allowEdit || !allowSelection || SelectedItem.Value == Model;
+ [Resolved]
+ private BeatmapLookupCache beatmapLookupCache { get; set; }
- public DrawableRoomPlaylistItem(PlaylistItem item, bool allowEdit, bool allowSelection, bool showItemOwner)
+ protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model;
+
+ public DrawableRoomPlaylistItem(PlaylistItem item)
: base(item)
{
Item = item;
- // TODO: edit support should be moved out into a derived class
- this.allowEdit = allowEdit;
- this.allowSelection = allowSelection;
- this.showItemOwner = showItemOwner;
-
beatmap.BindTo(item.Beatmap);
valid.BindTo(item.Valid);
ruleset.BindTo(item.Ruleset);
requiredMods.BindTo(item.RequiredMods);
- ShowDragHandle.Value = allowEdit;
-
if (item.Expired)
Colour = OsuColour.Gray(0.5f);
}
@@ -96,9 +118,6 @@ namespace osu.Game.Screens.OnlinePlay
[BackgroundDependencyLoader]
private void load()
{
- if (!allowEdit)
- HandleColour = HandleColour.Opacity(0);
-
maskingContainer.BorderColour = colours.Yellow;
}
@@ -130,10 +149,123 @@ namespace osu.Game.Screens.OnlinePlay
valid.BindValueChanged(_ => Scheduler.AddOnce(refresh));
requiredMods.CollectionChanged += (_, __) => Scheduler.AddOnce(refresh);
+ onScreenLoader.DelayedLoadStarted += _ =>
+ {
+ Task.Run(async () =>
+ {
+ try
+ {
+ if (showItemOwner)
+ {
+ var foundUser = await userLookupCache.GetUserAsync(Item.OwnerID).ConfigureAwait(false);
+ Schedule(() => ownerAvatar.User = foundUser);
+ }
+
+ if (Item.Beatmap.Value == null)
+ {
+ IBeatmapInfo foundBeatmap;
+
+ if (multiplayerClient != null)
+ // This call can eventually go away (and use the else case below).
+ // Currently required only due to the method being overridden to provide special behaviour in tests.
+ foundBeatmap = await multiplayerClient.GetAPIBeatmap(Item.BeatmapID).ConfigureAwait(false);
+ else
+ foundBeatmap = await beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ConfigureAwait(false);
+
+ Schedule(() => Item.Beatmap.Value = foundBeatmap);
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.Log($"Error while populating playlist item {e}");
+ }
+ });
+ };
+
refresh();
}
- private PanelBackground panelBackground;
+ ///
+ /// Whether this item can be selected.
+ ///
+ public bool AllowSelection { get; set; }
+
+ ///
+ /// Whether this item can be reordered in the playlist.
+ ///
+ public bool AllowReordering
+ {
+ get => ShowDragHandle.Value;
+ set => ShowDragHandle.Value = value;
+ }
+
+ private bool allowDeletion;
+
+ ///
+ /// Whether this item can be deleted.
+ ///
+ public bool AllowDeletion
+ {
+ get => allowDeletion;
+ set
+ {
+ allowDeletion = value;
+
+ if (removeButton != null)
+ removeButton.Alpha = value ? 1 : 0;
+ }
+ }
+
+ private bool allowShowingResults;
+
+ ///
+ /// Whether this item can have results shown.
+ ///
+ public bool AllowShowingResults
+ {
+ get => allowShowingResults;
+ set
+ {
+ allowShowingResults = value;
+
+ if (showResultsButton != null)
+ showResultsButton.Alpha = value ? 1 : 0;
+ }
+ }
+
+ private bool allowEditing;
+
+ ///
+ /// Whether this item can be edited.
+ ///
+ public bool AllowEditing
+ {
+ get => allowEditing;
+ set
+ {
+ allowEditing = value;
+
+ if (editButton != null)
+ editButton.Alpha = value ? 1 : 0;
+ }
+ }
+
+ private bool showItemOwner;
+
+ ///
+ /// Whether to display the avatar of the user which owns this playlist item.
+ ///
+ public bool ShowItemOwner
+ {
+ get => showItemOwner;
+ set
+ {
+ showItemOwner = value;
+
+ if (ownerAvatar != null)
+ ownerAvatar.Alpha = value ? 1 : 0;
+ }
+ }
private void refresh()
{
@@ -143,22 +275,22 @@ namespace osu.Game.Screens.OnlinePlay
maskingContainer.BorderColour = colours.Red;
}
- if (showItemOwner)
- {
- ownerAvatar.Show();
- userLookupCache.GetUserAsync(Item.OwnerID)
- .ContinueWith(u => Schedule(() => ownerAvatar.User = u.Result), TaskContinuationOptions.OnlyOnRanToCompletion);
- }
-
- difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(ICON_HEIGHT) };
+ if (Item.Beatmap.Value != null)
+ difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) };
+ else
+ difficultyIconContainer.Clear();
panelBackground.Beatmap.Value = Item.Beatmap.Value;
beatmapText.Clear();
- beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineID.ToString(), null, text =>
+
+ if (Item.Beatmap.Value != null)
{
- text.Truncate = true;
- });
+ beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineID.ToString(), null, text =>
+ {
+ text.Truncate = true;
+ });
+ }
authorText.Clear();
@@ -168,10 +300,16 @@ namespace osu.Game.Screens.OnlinePlay
authorText.AddUserLink(Item.Beatmap.Value.Metadata.Author);
}
- bool hasExplicitContent = (Item.Beatmap.Value.BeatmapSet as IBeatmapSetOnlineInfo)?.HasExplicitContent == true;
+ bool hasExplicitContent = (Item.Beatmap.Value?.BeatmapSet as IBeatmapSetOnlineInfo)?.HasExplicitContent == true;
explicitContentPill.Alpha = hasExplicitContent ? 1 : 0;
modDisplay.Current.Value = requiredMods.ToArray();
+
+ buttonsFlow.Clear();
+ buttonsFlow.ChildrenEnumerable = createButtons();
+
+ difficultyIconContainer.FadeInFromZero(500, Easing.OutQuint);
+ mainFillFlow.FadeInFromZero(500, Easing.OutQuint);
}
protected override Drawable CreateContent()
@@ -192,6 +330,7 @@ namespace osu.Game.Screens.OnlinePlay
Alpha = 0,
AlwaysPresent = true
},
+ onScreenLoader,
panelBackground = new PanelBackground
{
RelativeSizeAxes = Axes.Both,
@@ -217,7 +356,7 @@ namespace osu.Game.Screens.OnlinePlay
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Left = 8, Right = 8 },
},
- new FillFlowContainer
+ mainFillFlow = new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@@ -273,7 +412,7 @@ namespace osu.Game.Screens.OnlinePlay
}
}
},
- new FillFlowContainer
+ buttonsFlow = new FillFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
@@ -281,7 +420,7 @@ namespace osu.Game.Screens.OnlinePlay
Margin = new MarginPadding { Horizontal = 8 },
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
- ChildrenEnumerable = CreateButtons().Select(button => button.With(b =>
+ ChildrenEnumerable = createButtons().Select(button => button.With(b =>
{
b.Anchor = Anchor.Centre;
b.Origin = Anchor.Centre;
@@ -291,11 +430,11 @@ namespace osu.Game.Screens.OnlinePlay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Size = new Vector2(ICON_HEIGHT),
+ Size = new Vector2(icon_height),
Margin = new MarginPadding { Right = 8 },
Masking = true,
CornerRadius = 4,
- Alpha = showItemOwner ? 1 : 0
+ Alpha = ShowItemOwner ? 1 : 0
},
}
}
@@ -304,38 +443,53 @@ namespace osu.Game.Screens.OnlinePlay
};
}
- protected virtual IEnumerable CreateButtons() =>
- new Drawable[]
+ private IEnumerable createButtons() => new[]
+ {
+ showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie)
{
- new PlaylistDownloadButton(Item),
- new PlaylistRemoveButton
- {
- Size = new Vector2(30, 30),
- Alpha = allowEdit ? 1 : 0,
- Action = () => RequestDeletion?.Invoke(Model),
- },
- };
+ Size = new Vector2(30, 30),
+ Action = () => RequestResults?.Invoke(Item),
+ Alpha = AllowShowingResults ? 1 : 0,
+ TooltipText = "View results"
+ },
+ Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item),
+ editButton = new PlaylistEditButton
+ {
+ Size = new Vector2(30, 30),
+ Alpha = AllowEditing ? 1 : 0,
+ Action = () => RequestEdit?.Invoke(Item),
+ TooltipText = "Edit"
+ },
+ removeButton = new PlaylistRemoveButton
+ {
+ Size = new Vector2(30, 30),
+ Alpha = AllowDeletion ? 1 : 0,
+ Action = () => RequestDeletion?.Invoke(Item),
+ TooltipText = "Remove from playlist"
+ },
+ };
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ if (AllowSelection && valid.Value)
+ SelectedItem.Value = Model;
+ return true;
+ }
+
+ public class PlaylistEditButton : GrayButton
+ {
+ public PlaylistEditButton()
+ : base(FontAwesome.Solid.Edit)
+ {
+ }
+ }
public class PlaylistRemoveButton : GrayButton
{
public PlaylistRemoveButton()
: base(FontAwesome.Solid.MinusSquare)
{
- TooltipText = "Remove from playlist";
}
-
- [BackgroundDependencyLoader]
- private void load()
- {
- Icon.Scale = new Vector2(0.8f);
- }
- }
-
- protected override bool OnClick(ClickEvent e)
- {
- if (allowSelection && valid.Value)
- SelectedItem.Value = Model;
- return true;
}
private sealed class PlaylistDownloadButton : BeatmapDownloadButton
diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs
deleted file mode 100644
index 8b1bb7abc1..0000000000
--- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Online.Rooms;
-
-namespace osu.Game.Screens.OnlinePlay
-{
- public class DrawableRoomPlaylistWithResults : DrawableRoomPlaylist
- {
- public Action RequestShowResults;
-
- private readonly bool showItemOwner;
-
- public DrawableRoomPlaylistWithResults(bool showItemOwner = false)
- : base(false, true, showItemOwner: showItemOwner)
- {
- this.showItemOwner = showItemOwner;
- }
-
- protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) =>
- new DrawableRoomPlaylistItemWithResults(item, false, true, showItemOwner)
- {
- RequestShowResults = () => RequestShowResults(item),
- SelectedItem = { BindTarget = SelectedItem },
- };
-
- private class DrawableRoomPlaylistItemWithResults : DrawableRoomPlaylistItem
- {
- public Action RequestShowResults;
-
- public DrawableRoomPlaylistItemWithResults(PlaylistItem item, bool allowEdit, bool allowSelection, bool showItemOwner)
- : base(item, allowEdit, allowSelection, showItemOwner)
- {
- }
-
- protected override IEnumerable CreateButtons() =>
- base.CreateButtons().Prepend(new FilledIconButton
- {
- Icon = FontAwesome.Solid.ChartPie,
- Action = () => RequestShowResults?.Invoke(),
- TooltipText = "View results"
- });
-
- private class FilledIconButton : IconButton
- {
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- Add(new Box
- {
- RelativeSizeAxes = Axes.Both,
- Depth = float.MaxValue,
- Colour = colours.Gray4,
- });
- }
- }
- }
- }
-}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs
index 9920883078..a87f21630c 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -30,9 +31,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
public readonly Room Room;
- [Resolved]
- private BeatmapManager beatmaps { get; set; }
-
protected Container ButtonsContainer { get; private set; }
private readonly Bindable roomType = new Bindable();
@@ -187,20 +185,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
- Children = new Drawable[]
- {
- new PlaylistCountPill
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- },
- new StarRatingRangeDisplay
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Scale = new Vector2(0.8f)
- }
- }
+ ChildrenEnumerable = CreateBottomDetails()
}
}
},
@@ -290,6 +275,37 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
protected virtual Drawable CreateBackground() => new OnlinePlayBackgroundSprite();
+ protected virtual IEnumerable CreateBottomDetails()
+ {
+ var pills = new List();
+
+ if (Room.Type.Value != MatchType.Playlists)
+ {
+ pills.AddRange(new OnlinePlayComposite[]
+ {
+ new MatchTypePill(),
+ new QueueModePill(),
+ });
+ }
+
+ pills.AddRange(new Drawable[]
+ {
+ new PlaylistCountPill
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
+ new StarRatingRangeDisplay
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Scale = new Vector2(0.8f)
+ }
+ });
+
+ return pills;
+ }
+
private class RoomNameText : OsuSpriteText
{
[Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))]
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs
new file mode 100644
index 0000000000..d104ede8f7
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.Rooms;
+
+namespace osu.Game.Screens.OnlinePlay.Lounge.Components
+{
+ public class MatchTypePill : OnlinePlayComposite
+ {
+ private OsuTextFlowContainer textFlow;
+
+ public MatchTypePill()
+ {
+ AutoSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = new PillContainer
+ {
+ Child = textFlow = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ AutoSizeAxes = Axes.Both,
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Type.BindValueChanged(onMatchTypeChanged, true);
+ }
+
+ private void onMatchTypeChanged(ValueChangedEvent type)
+ {
+ textFlow.Clear();
+ textFlow.AddText(type.NewValue.GetLocalisableDescription());
+ }
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs
new file mode 100644
index 0000000000..7501f0237b
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.Multiplayer;
+
+namespace osu.Game.Screens.OnlinePlay.Lounge.Components
+{
+ public class QueueModePill : OnlinePlayComposite
+ {
+ private OsuTextFlowContainer textFlow;
+
+ public QueueModePill()
+ {
+ AutoSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = new PillContainer
+ {
+ Child = textFlow = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ AutoSizeAxes = Axes.Both,
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ QueueMode.BindValueChanged(onQueueModeChanged, true);
+ }
+
+ private void onQueueModeChanged(ValueChangedEvent mode)
+ {
+ textFlow.Clear();
+ textFlow.AddText(mode.NewValue.GetLocalisableDescription());
+ }
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
index 54c762b8ce..f4d7823fcc 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
@@ -33,9 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
[Resolved]
private IRoomManager roomManager { get; set; }
- [Resolved(CanBeNull = true)]
- private LoungeSubScreen loungeSubScreen { get; set; }
-
// handle deselection
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
index 7c5ed3f5cc..a560d85b7d 100644
--- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
public abstract class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner
{
[Cached(typeof(IBindable))]
- protected readonly Bindable SelectedItem = new Bindable();
+ public readonly Bindable SelectedItem = new Bindable();
public override bool? AllowTrackAdjustments => true;
@@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
protected OnlinePlayScreen ParentScreen { get; private set; }
[Cached]
- private OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker { get; set; }
+ private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
protected IBindable BeatmapAvailability => beatmapAvailabilityTracker.Availability;
@@ -90,11 +90,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
Padding = new MarginPadding { Top = Header.HEIGHT };
- beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
- {
- SelectedItem = { BindTarget = SelectedItem }
- };
-
RoomId.BindTo(room.RoomID);
}
@@ -247,10 +242,10 @@ namespace osu.Game.Screens.OnlinePlay.Match
}, true);
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged));
-
- beatmapManager.ItemUpdated += beatmapUpdated;
-
UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods));
+
+ beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem);
+ beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateWorkingBeatmap());
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@@ -319,6 +314,16 @@ namespace osu.Game.Screens.OnlinePlay.Match
protected void StartPlay()
{
+ // User may be at song select or otherwise when the host starts gameplay.
+ // Ensure that they first return to this screen, else global bindables (beatmap etc.) may be in a bad state.
+ if (!this.IsCurrentScreen())
+ {
+ this.MakeCurrent();
+
+ Schedule(StartPlay);
+ return;
+ }
+
sampleStart?.Play();
// fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes).
@@ -364,8 +369,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
}
}
- private void beatmapUpdated(BeatmapSetInfo set) => Schedule(updateWorkingBeatmap);
-
private void updateWorkingBeatmap()
{
var beatmap = SelectedItem.Value?.Beatmap.Value;
@@ -433,14 +436,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
/// The room to change the settings of.
protected abstract RoomSettingsOverlay CreateRoomSettingsOverlay(Room room);
- protected override void Dispose(bool isDisposing)
- {
- base.Dispose(isDisposing);
-
- if (beatmapManager != null)
- beatmapManager.ItemUpdated -= beatmapUpdated;
- }
-
public class UserModSelectButton : PurpleTriangleButton
{
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
index 34edc1ccd1..7f1db733b3 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
@@ -12,7 +12,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Screens;
-using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -20,7 +19,6 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
-using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osuTK;
@@ -84,12 +82,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
[Resolved]
private MultiplayerClient client { get; set; }
- [Resolved]
- private Bindable beatmap { get; set; }
-
- [Resolved]
- private Bindable ruleset { get; set; }
-
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
@@ -256,7 +248,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Spacing = new Vector2(5),
Children = new Drawable[]
{
- drawablePlaylist = new DrawableRoomPlaylist(false, false)
+ drawablePlaylist = new DrawableRoomPlaylist
{
RelativeSizeAxes = Axes.X,
Height = DrawableRoomPlaylistItem.HEIGHT
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
index ce988e377f..06959d942f 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
@@ -11,7 +11,6 @@ using osu.Framework.Graphics;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
-using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.OnlinePlay.Components;
using osuTK;
@@ -25,9 +24,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
set => button.Action = value;
}
- [Resolved]
- private IAPIProvider api { get; set; }
-
[Resolved]
private OsuColour colours { get; set; }
@@ -67,6 +63,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
sampleUnready = audio.Samples.Get(@"Multiplayer/player-unready");
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ SelectedItem.BindValueChanged(_ => updateState());
+ }
+
protected override void OnRoomUpdated()
{
base.OnRoomUpdated();
@@ -108,7 +111,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
bool enableButton =
Room?.State == MultiplayerRoomState.Open
- && Client.CurrentMatchPlayingItem.Value?.Expired == false
+ && SelectedItem.Value?.ID == Room.Settings.PlaylistItemId
+ && !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
&& !operationInProgress.Value;
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready.
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs
new file mode 100644
index 0000000000..32d355d149
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Online.Rooms;
+using osuTK;
+
+namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
+{
+ ///
+ /// A historically-ordered list of s.
+ ///
+ public class MultiplayerHistoryList : DrawableRoomPlaylist
+ {
+ public MultiplayerHistoryList()
+ {
+ ShowItemOwners = true;
+ }
+
+ protected override FillFlowContainer> CreateListFillFlowContainer() => new HistoryFillFlowContainer
+ {
+ Spacing = new Vector2(0, 2)
+ };
+
+ private class HistoryFillFlowContainer : FillFlowContainer>
+ {
+ public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderByDescending(item => item.Model.PlayedAt);
+ }
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs
new file mode 100644
index 0000000000..7b90532cce
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs
@@ -0,0 +1,142 @@
+// Copyright (c) ppy Pty Ltd . 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.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
+
+namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
+{
+ ///
+ /// The multiplayer playlist, containing lists to show the items from a in both gameplay-order and historical-order.
+ ///
+ public class MultiplayerPlaylist : MultiplayerRoomComposite
+ {
+ public readonly Bindable DisplayMode = new Bindable();
+
+ ///
+ /// Invoked when an item requests to be edited.
+ ///
+ public Action RequestEdit;
+
+ private MultiplayerQueueList queueList;
+ private MultiplayerHistoryList historyList;
+ private bool firstPopulation = true;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ const float tab_control_height = 25;
+
+ InternalChildren = new Drawable[]
+ {
+ new OsuTabControl
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = tab_control_height,
+ Current = { BindTarget = DisplayMode }
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Top = tab_control_height + 5 },
+ Masking = true,
+ Children = new Drawable[]
+ {
+ queueList = new MultiplayerQueueList
+ {
+ RelativeSizeAxes = Axes.Both,
+ SelectedItem = { BindTarget = SelectedItem },
+ RequestEdit = item => RequestEdit?.Invoke(item)
+ },
+ historyList = new MultiplayerHistoryList
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ SelectedItem = { BindTarget = SelectedItem }
+ }
+ }
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ DisplayMode.BindValueChanged(onDisplayModeChanged, true);
+ }
+
+ private void onDisplayModeChanged(ValueChangedEvent mode)
+ {
+ historyList.FadeTo(mode.NewValue == MultiplayerPlaylistDisplayMode.History ? 1 : 0, 100);
+ queueList.FadeTo(mode.NewValue == MultiplayerPlaylistDisplayMode.Queue ? 1 : 0, 100);
+ }
+
+ protected override void OnRoomUpdated()
+ {
+ base.OnRoomUpdated();
+
+ if (Room == null)
+ {
+ historyList.Items.Clear();
+ queueList.Items.Clear();
+ firstPopulation = true;
+ return;
+ }
+
+ if (firstPopulation)
+ {
+ foreach (var item in Room.Playlist)
+ addItemToLists(item);
+
+ firstPopulation = false;
+ }
+ }
+
+ protected override void PlaylistItemAdded(MultiplayerPlaylistItem item)
+ {
+ base.PlaylistItemAdded(item);
+ addItemToLists(item);
+ }
+
+ protected override void PlaylistItemRemoved(long item)
+ {
+ base.PlaylistItemRemoved(item);
+ removeItemFromLists(item);
+ }
+
+ protected override void PlaylistItemChanged(MultiplayerPlaylistItem item)
+ {
+ base.PlaylistItemChanged(item);
+
+ removeItemFromLists(item.ID);
+ addItemToLists(item);
+ }
+
+ private void addItemToLists(MultiplayerPlaylistItem item)
+ {
+ var apiItem = Playlist.SingleOrDefault(i => i.ID == item.ID);
+
+ // Item could have been removed from the playlist while the local player was in gameplay.
+ if (apiItem == null)
+ return;
+
+ if (item.Expired)
+ historyList.Items.Add(apiItem);
+ else
+ queueList.Items.Add(apiItem);
+ }
+
+ private void removeItemFromLists(long item)
+ {
+ queueList.Items.RemoveAll(i => i.ID == item);
+ historyList.Items.RemoveAll(i => i.ID == item);
+ }
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistDisplayMode.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistDisplayMode.cs
new file mode 100644
index 0000000000..cc3dca6a34
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistDisplayMode.cs
@@ -0,0 +1,14 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
+{
+ ///
+ /// The type of list displayed in a .
+ ///
+ public enum MultiplayerPlaylistDisplayMode
+ {
+ Queue,
+ History,
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
new file mode 100644
index 0000000000..3e0f663d42
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
@@ -0,0 +1,88 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
+using osuTK;
+
+namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
+{
+ ///
+ /// A gameplay-ordered list of s.
+ ///
+ public class MultiplayerQueueList : DrawableRoomPlaylist
+ {
+ public MultiplayerQueueList()
+ {
+ ShowItemOwners = true;
+ }
+
+ protected override FillFlowContainer> CreateListFillFlowContainer() => new QueueFillFlowContainer
+ {
+ Spacing = new Vector2(0, 2)
+ };
+
+ protected override DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new QueuePlaylistItem(item);
+
+ private class QueueFillFlowContainer : FillFlowContainer>
+ {
+ [Resolved(typeof(Room), nameof(Room.Playlist))]
+ private BindableList roomPlaylist { get; set; }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ roomPlaylist.BindCollectionChanged((_, __) => InvalidateLayout());
+ }
+
+ public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderBy(item => item.Model.PlaylistOrder);
+ }
+
+ private class QueuePlaylistItem : DrawableRoomPlaylistItem
+ {
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ [Resolved]
+ private MultiplayerClient multiplayerClient { get; set; }
+
+ [Resolved(typeof(Room), nameof(Room.Host))]
+ private Bindable host { get; set; }
+
+ [Resolved(typeof(Room), nameof(Room.QueueMode))]
+ private Bindable queueMode { get; set; }
+
+ public QueuePlaylistItem(PlaylistItem item)
+ : base(item)
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ RequestDeletion = item => multiplayerClient.RemovePlaylistItem(item.ID);
+
+ host.BindValueChanged(_ => updateDeleteButtonVisibility());
+ queueMode.BindValueChanged(_ => updateDeleteButtonVisibility());
+ SelectedItem.BindValueChanged(_ => updateDeleteButtonVisibility(), true);
+ }
+
+ private void updateDeleteButtonVisibility()
+ {
+ bool isItemOwner = Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost;
+
+ AllowDeletion = isItemOwner && SelectedItem.Value != Item;
+ AllowEditing = isItemOwner;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
index 58b5b7bbeb..e136627d43 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Online.Multiplayer;
@@ -18,8 +19,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.OnResuming(last);
- if (client.Room != null && client.LocalUser?.State != MultiplayerUserState.Spectating)
- client.ChangeState(MultiplayerUserState.Idle);
+ if (client.Room == null)
+ return;
+
+ Debug.Assert(client.LocalUser != null);
+
+ switch (client.LocalUser.State)
+ {
+ case MultiplayerUserState.Spectating:
+ break;
+
+ case MultiplayerUserState.WaitingForLoad:
+ case MultiplayerUserState.Loaded:
+ case MultiplayerUserState.Playing:
+ client.AbortGameplay();
+ break;
+
+ default:
+ client.ChangeState(MultiplayerUserState.Idle);
+ break;
+ }
}
protected override string ScreenTitle => "Multiplayer";
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
index 44efef53f5..073497e1ce 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
@@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using System.Linq;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using osu.Framework.Allocation;
using osu.Framework.Logging;
@@ -24,17 +25,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved]
private MultiplayerClient client { get; set; }
+ private readonly long? itemToEdit;
+
private LoadingLayer loadingLayer;
///
/// Construct a new instance of multiplayer song select.
///
/// The room.
+ /// The item to be edited. May be null, in which case a new item will be added to the playlist.
/// An optional initial beatmap selection to perform.
/// An optional initial ruleset selection to perform.
- public MultiplayerMatchSongSelect(Room room, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
+ public MultiplayerMatchSongSelect(Room room, long? itemToEdit = null, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
: base(room)
{
+ this.itemToEdit = itemToEdit;
+
if (beatmap != null || ruleset != null)
{
Schedule(() =>
@@ -59,14 +65,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
loadingLayer.Show();
- client.AddPlaylistItem(new MultiplayerPlaylistItem
+ var multiplayerItem = new MultiplayerPlaylistItem
{
+ ID = itemToEdit ?? 0,
BeatmapID = item.BeatmapID,
BeatmapChecksum = item.Beatmap.Value.MD5Hash,
RulesetID = item.RulesetID,
RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray(),
AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray()
- }).ContinueWith(t =>
+ };
+
+ Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem);
+
+ task.ContinueWith(t =>
{
Schedule(() =>
{
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index 077e9cef93..6895608c8e 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -14,7 +15,6 @@ using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
@@ -26,6 +26,7 @@ using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
+using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
@@ -43,8 +44,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
public override string ShortTitle => "room";
- public OsuButton AddOrEditPlaylistButton { get; private set; }
-
[Resolved]
private MultiplayerClient client { get; set; }
@@ -56,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[CanBeNull]
private IDisposable readyClickOperation;
- private DrawableRoomPlaylist playlist;
+ private AddItemButton addItemButton;
public MultiplayerMatchSubScreen(Room room)
: base(room)
@@ -69,14 +68,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.LoadComplete();
- SelectedItem.BindTo(client.CurrentMatchPlayingItem);
-
BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true);
UserMods.BindValueChanged(onUserModsChanged);
- playlist.Items.BindTo(Room.Playlist);
- playlist.SelectedItem.BindTo(SelectedItem);
-
client.LoadRequested += onLoadRequested;
client.RoomUpdated += onRoomUpdated;
@@ -138,25 +132,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
new Drawable[] { new OverlinedHeader("Beatmap") },
new Drawable[]
{
- AddOrEditPlaylistButton = new PurpleTriangleButton
+ addItemButton = new AddItemButton
{
RelativeSizeAxes = Axes.X,
Height = 40,
- Action = () =>
- {
- if (this.IsCurrentScreen())
- this.Push(new MultiplayerMatchSongSelect(Room));
- },
- Alpha = 0
+ Text = "Add item",
+ Action = () => OpenSongSelection()
},
},
null,
new Drawable[]
{
- playlist = new DrawableRoomPlaylist(false, false, true, true)
+ new MultiplayerPlaylist
{
RelativeSizeAxes = Axes.Both,
- },
+ RequestEdit = item => OpenSongSelection(item.ID)
+ }
},
new[]
{
@@ -228,6 +219,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
};
+ ///
+ /// Opens the song selection screen to add or edit an item.
+ ///
+ /// An optional playlist item to edit. If null, a new item will be added instead.
+ internal void OpenSongSelection(long? itemToEdit = null)
+ {
+ if (!this.IsCurrentScreen())
+ return;
+
+ this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit));
+ }
+
protected override Drawable CreateFooter() => new MultiplayerMatchFooter
{
OnReadyClick = onReadyClick,
@@ -323,10 +326,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.LocalUser?.State == MultiplayerUserState.Ready)
client.ChangeState(MultiplayerUserState.Idle);
}
- else
+ else if (client.LocalUser?.State == MultiplayerUserState.Spectating
+ && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing))
{
- if (client.LocalUser?.State == MultiplayerUserState.Spectating && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing))
- onLoadRequested();
+ onLoadRequested();
}
}
@@ -385,27 +388,50 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return;
}
- switch (client.Room.Settings.QueueMode)
- {
- case QueueMode.HostOnly:
- AddOrEditPlaylistButton.Text = "Edit beatmap";
- AddOrEditPlaylistButton.Alpha = client.IsHost ? 1 : 0;
- break;
+ updateCurrentItem();
- case QueueMode.AllPlayers:
- case QueueMode.AllPlayersRoundRobin:
- AddOrEditPlaylistButton.Text = "Add beatmap";
- AddOrEditPlaylistButton.Alpha = 1;
- break;
-
- default:
- AddOrEditPlaylistButton.Alpha = 0;
- break;
- }
+ addItemButton.Alpha = client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly ? 1 : 0;
Scheduler.AddOnce(UpdateMods);
}
+ private void updateCurrentItem()
+ {
+ Debug.Assert(client.Room != null);
+
+ var expectedSelectedItem = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId);
+
+ if (expectedSelectedItem == null)
+ return;
+
+ // There's no reason to renew the selected item if its content hasn't changed.
+ if (SelectedItem.Value?.Equals(expectedSelectedItem) == true && expectedSelectedItem.Beatmap.Value != null)
+ return;
+
+ // Clear the selected item while the lookup is performed, so components like the ready button can enter their disabled states.
+ SelectedItem.Value = null;
+
+ if (expectedSelectedItem.Beatmap.Value == null)
+ {
+ Task.Run(async () =>
+ {
+ var beatmap = await client.GetAPIBeatmap(expectedSelectedItem.BeatmapID).ConfigureAwait(false);
+
+ Schedule(() =>
+ {
+ expectedSelectedItem.Beatmap.Value = beatmap;
+
+ if (Room.Playlist.SingleOrDefault(i => i.ID == client.Room?.Settings.PlaylistItemId)?.Equals(expectedSelectedItem) == true)
+ applyCurrentItem();
+ });
+ });
+ }
+ else
+ applyCurrentItem();
+
+ void applyCurrentItem() => SelectedItem.Value = expectedSelectedItem;
+ }
+
private void handleRoomLost() => Schedule(() =>
{
if (this.IsCurrentScreen())
@@ -458,6 +484,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!this.IsCurrentScreen())
return;
+ if (client.Room == null)
+ return;
+
if (!client.IsHost)
{
// todo: should handle this when the request queue is implemented.
@@ -466,7 +495,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return;
}
- this.Push(new MultiplayerMatchSongSelect(Room, beatmap, ruleset));
+ this.Push(new MultiplayerMatchSongSelect(Room, client.Room.Settings.PlaylistItemId, beatmap, ruleset));
}
protected override void Dispose(bool isDisposing)
@@ -481,5 +510,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
modSettingChangeTracker?.Dispose();
}
+
+ public class AddItemButton : PurpleTriangleButton
+ {
+ }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
index a380ddef25..7d2fe44c4e 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
@@ -4,6 +4,7 @@
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
@@ -23,14 +24,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Client.UserLeft += invokeUserLeft;
Client.UserKicked += invokeUserKicked;
Client.UserJoined += invokeUserJoined;
+ Client.ItemAdded += invokeItemAdded;
+ Client.ItemRemoved += invokeItemRemoved;
+ Client.ItemChanged += invokeItemChanged;
OnRoomUpdated();
}
private void invokeOnRoomUpdated() => Scheduler.AddOnce(OnRoomUpdated);
- private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.AddOnce(UserJoined, user);
- private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.AddOnce(UserKicked, user);
- private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.AddOnce(UserLeft, user);
+ private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => UserJoined(user));
+ private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.Add(() => UserKicked(user));
+ private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => UserLeft(user));
+ private void invokeItemAdded(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemAdded(item));
+ private void invokeItemRemoved(long item) => Schedule(() => PlaylistItemRemoved(item));
+ private void invokeItemChanged(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemChanged(item));
///
/// Invoked when a user has joined the room.
@@ -56,6 +63,30 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
}
+ ///
+ /// Invoked when a playlist item is added to the room.
+ ///
+ /// The added playlist item.
+ protected virtual void PlaylistItemAdded(MultiplayerPlaylistItem item)
+ {
+ }
+
+ ///
+ /// Invoked when a playlist item is removed from the room.
+ ///
+ /// The ID of the removed playlist item.
+ protected virtual void PlaylistItemRemoved(long item)
+ {
+ }
+
+ ///
+ /// Invoked when a playlist item is changed in the room.
+ ///
+ /// The new playlist item, with an existing item's ID.
+ protected virtual void PlaylistItemChanged(MultiplayerPlaylistItem item)
+ {
+ }
+
///
/// Invoked when any change occurs to the multiplayer room.
///
@@ -71,6 +102,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Client.UserLeft -= invokeUserLeft;
Client.UserKicked -= invokeUserKicked;
Client.UserJoined -= invokeUserJoined;
+ Client.ItemAdded -= invokeItemAdded;
+ Client.ItemRemoved -= invokeItemRemoved;
+ Client.ItemChanged -= invokeItemChanged;
}
base.Dispose(isDisposing);
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
index 3152f50d3d..8fbaebadfe 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
private IAPIProvider api { get; set; }
[Resolved]
- private RulesetStore rulesets { get; set; }
+ private IRulesetStore rulesets { get; set; }
private SpriteIcon crown;
@@ -185,9 +185,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
const double fade_time = 50;
// Todo: Should use the room's selected item to determine ruleset.
- var ruleset = rulesets.GetRuleset(0).CreateInstance();
+ var ruleset = rulesets.GetRuleset(0)?.CreateInstance();
- int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank;
+ int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null;
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs
index 2ad64e115e..d36c556fac 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs
@@ -77,7 +77,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
else
{
// Remove panels for users no longer in the room.
- panels.RemoveAll(p => !Room.Users.Contains(p.User));
+ foreach (var p in panels)
+ {
+ // Note that we *must* use reference equality here, as this call is scheduled and a user may have left and joined since it was last run.
+ if (Room.Users.All(u => !ReferenceEquals(p.User, u)))
+ p.Expire();
+ }
// Add panels for all users new to the room.
foreach (var user in Room.Users.Except(panels.Select(p => p.User)))
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
index 57d0d2c198..7350408eba 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@@ -36,9 +37,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
[Resolved]
private OsuColour colours { get; set; }
- [Resolved]
- private SpectatorClient spectatorClient { get; set; }
-
[Resolved]
private MultiplayerClient multiplayerClient { get; set; }
@@ -229,8 +227,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public override bool OnBackButton()
{
- // On a manual exit, set the player state back to idle.
- multiplayerClient.ChangeState(MultiplayerUserState.Idle);
+ Debug.Assert(multiplayerClient.Room != null);
+
+ // On a manual exit, set the player back to idle unless gameplay has finished.
+ if (multiplayerClient.Room.State != MultiplayerRoomState.Open)
+ multiplayerClient.ChangeState(MultiplayerUserState.Idle);
+
return base.OnBackButton();
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs
index c3190cd845..48f153ecbe 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs
@@ -87,7 +87,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
gameplayContent.Child = new PlayerIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.BeatmapInfo), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods)
{
RelativeSizeAxes = Axes.Both,
- Child = stack = new OsuScreenStack()
+ Child = stack = new OsuScreenStack
+ {
+ Name = nameof(PlayerArea),
+ }
};
stack.Push(new MultiSpectatorPlayerLoader(Score, () => new MultiSpectatorPlayer(Score, GameplayClock)));
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
index a18e4b45cf..19153521cd 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
@@ -40,18 +40,9 @@ namespace osu.Game.Screens.OnlinePlay
[Cached]
private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker();
- [Resolved(CanBeNull = true)]
- private MusicController music { get; set; }
-
- [Resolved]
- private OsuGameBase game { get; set; }
-
[Resolved]
protected IAPIProvider API { get; private set; }
- [Resolved(CanBeNull = true)]
- private OsuLogo logo { get; set; }
-
protected OnlinePlayScreen()
{
Anchor = Anchor.Centre;
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs
index 4bc0b55433..63957caee3 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs
@@ -33,14 +33,14 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList Playlist { get; private set; }
+ [CanBeNull]
+ [Resolved(CanBeNull = true)]
+ protected IBindable SelectedItem { get; private set; }
+
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
protected readonly Bindable> FreeMods = new Bindable>(Array.Empty());
- [CanBeNull]
- [Resolved(CanBeNull = true)]
- private IBindable selectedItem { get; set; }
-
private readonly FreeModSelectOverlay freeModSelectOverlay;
private readonly Room room;
@@ -80,8 +80,8 @@ namespace osu.Game.Screens.OnlinePlay
// At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods.
// Similarly, freeMods is currently empty but should only contain the allowed mods.
- Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty();
- FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty();
+ Mods.Value = SelectedItem?.Value?.RequiredMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty();
+ FreeMods.Value = SelectedItem?.Value?.AllowedMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty();
Mods.BindValueChanged(onModsChanged);
Ruleset.BindValueChanged(onRulesetChanged);
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
index aed3635cbc..1e6722d51e 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
@@ -87,6 +87,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
var allScores = new List { userScore };
+ // Other scores could have arrived between score submission and entering the results screen. Ensure the local player score position is up to date.
+ if (Score != null)
+ {
+ Score.Position = userScore.Position;
+ ScorePanelList.GetPanelForScore(Score).ScorePosition.Value = userScore.Position;
+ }
+
if (userScore.ScoresAround?.Higher != null)
{
allScores.AddRange(userScore.ScoresAround.Higher.Scores);
@@ -186,12 +193,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Schedule(() =>
{
// Prefer selecting the local user's score, or otherwise default to the first visible score.
- SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
+ SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
});
}
// Invoke callback to add the scores. Exclude the user's current score which was added previously.
- callback.Invoke(scoreInfos.Where(s => s.OnlineScoreID != Score?.OnlineScoreID));
+ callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID));
hideLoadingSpinners(pivot);
}));
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
index 27c8dc1120..6c8ab52d22 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
@@ -5,7 +5,9 @@ using System;
using System.Collections.Specialized;
using System.Linq;
using Humanizer;
+using Humanizer.Localisation;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -14,6 +16,8 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Match.Components;
@@ -69,6 +73,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved(CanBeNull = true)]
private IRoomManager manager { get; set; }
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
private readonly Room room;
public MatchSettings(Room room)
@@ -134,19 +141,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Child = DurationField = new DurationDropdown
{
RelativeSizeAxes = Axes.X,
- Items = new[]
- {
- TimeSpan.FromMinutes(30),
- TimeSpan.FromHours(1),
- TimeSpan.FromHours(2),
- TimeSpan.FromHours(4),
- TimeSpan.FromHours(8),
- TimeSpan.FromHours(12),
- //TimeSpan.FromHours(16),
- TimeSpan.FromHours(24),
- TimeSpan.FromDays(3),
- TimeSpan.FromDays(7)
- }
}
},
new Section("Allowed attempts (across all playlist items)")
@@ -205,7 +199,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
new Drawable[]
{
- playlist = new DrawableRoomPlaylist(true, false) { RelativeSizeAxes = Axes.Both }
+ playlist = new PlaylistsRoomSettingsPlaylist
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
},
new Drawable[]
{
@@ -300,10 +297,40 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
MaxAttempts.BindValueChanged(count => MaxAttemptsField.Text = count.NewValue?.ToString(), true);
Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true);
+ api.LocalUser.BindValueChanged(populateDurations, true);
+
playlist.Items.BindTo(Playlist);
Playlist.BindCollectionChanged(onPlaylistChanged, true);
}
+ private void populateDurations(ValueChangedEvent user)
+ {
+ DurationField.Items = new[]
+ {
+ TimeSpan.FromMinutes(30),
+ TimeSpan.FromHours(1),
+ TimeSpan.FromHours(2),
+ TimeSpan.FromHours(4),
+ TimeSpan.FromHours(8),
+ TimeSpan.FromHours(12),
+ TimeSpan.FromHours(24),
+ TimeSpan.FromDays(3),
+ TimeSpan.FromDays(7),
+ TimeSpan.FromDays(14),
+ };
+
+ // TODO: show these in the interface at all times.
+ if (user.NewValue.IsSupporter)
+ {
+ // roughly correct (see https://github.com/Humanizr/Humanizer/blob/18167e56c082449cc4fe805b8429e3127a7b7f93/readme.md?plain=1#L427)
+ // if we want this to be more accurate we might consider sending an actual end time, not a time span. probably not required though.
+ const int days_in_month = 31;
+
+ DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month));
+ DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month * 3));
+ }
+ }
+
protected override void Update()
{
base.Update();
@@ -402,7 +429,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Menu.MaxHeight = 100;
}
- protected override LocalisableString GenerateItemText(TimeSpan item) => item.Humanize();
+ protected override LocalisableString GenerateItemText(TimeSpan item) => item.Humanize(maxUnit: TimeUnit.Month);
}
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs
new file mode 100644
index 0000000000..2fe215eef2
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd