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/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/README.md b/README.md
index 786ce2589d..24b70b2de6 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
diff --git a/osu.Android.props b/osu.Android.props
index 0c922c09ac..563836d29b 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
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 a9e3575a49..7ec7d53a7e 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -90,7 +90,6 @@ namespace osu.Desktop
Logger.Log("Starting legacy IPC provider...");
legacyIpc = new LegacyTcpIpcProvider();
legacyIpc.Bind();
- legacyIpc.StartAsync();
}
catch (Exception ex)
{
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/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/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
index 91f5f93905..a30e09cd29 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
@@ -56,13 +56,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestDefaultSkin()
{
- AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLive());
+ AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLiveUnmanaged());
}
[Test]
public void TestLegacySkin()
{
- AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.CreateInfo().ToLive());
+ AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged());
}
}
}
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 466f0556ab..44ba0e2057 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
@@ -56,7 +56,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, (osuPrevious.JumpDistance / scalingFactor) / 25.0);
+ double stackNerf = Math.Min(1.0, (osuPrevious.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 4f87767fa7..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.MovementDistance);
+ 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.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
index a73ae9dcdb..81d89359e0 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
@@ -62,7 +62,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
public void TestCultureInvariance()
{
var ruleset = new OsuRuleset().RulesetInfo;
- var scoreInfo = new TestScoreInfo(ruleset);
+ var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset);
var score = new Score
{
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/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs
index a6edd6cb5f..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());
diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs
index 9b6769b788..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);
@@ -74,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();
@@ -103,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();
@@ -123,7 +151,7 @@ namespace osu.Game.Tests.Database
RunTestWithRealm((realmFactory, _) =>
{
var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
- var liveBeatmap = beatmap.ToLive();
+ var liveBeatmap = beatmap.ToLive(realmFactory);
Assert.DoesNotThrow(() =>
{
@@ -145,7 +173,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();
@@ -183,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();
@@ -222,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();
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/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 88f35976ad..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;
@@ -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/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
index 42305ccd81..bc0041e2c2 100644
--- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
+++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
@@ -45,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);
@@ -64,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/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/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
index bdd1b92c8d..3211405670 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
@@ -5,8 +5,11 @@ 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;
@@ -15,6 +18,7 @@ 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
{
@@ -22,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]
@@ -36,10 +39,95 @@ 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());
+
+ 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()
{
@@ -78,36 +166,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) =>
@@ -120,10 +196,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().ToLive());
+ 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 2a308dd0d4..55dbf89334 100644
--- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs
+++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs
@@ -6,12 +6,12 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
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;
@@ -19,11 +19,10 @@ 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.
@@ -254,14 +253,32 @@ namespace osu.Game.Tests.Visual.Beatmaps
public void TestNormal()
{
createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo));
+ }
- AddToggleStep("toggle expanded state", expanded =>
- {
- var card = this.ChildrenOfType().Last();
- if (!card.Expanded.Disabled)
- card.Expanded.Value = expanded;
- });
- AddToggleStep("disable/enable expansion", disabled => this.ChildrenOfType().ForEach(card => card.Expanded.Disabled = disabled));
+ [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();
}
[Test]
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 cccc962a3f..c5f56cae9e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("setup skins", () =>
{
- skinManager.CurrentSkinInfo.Value = gameCurrentSkin.ToLive();
+ 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/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/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/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 2411f39ae3..710855a605 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;
@@ -391,9 +393,9 @@ 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);
}
@@ -413,11 +415,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
+ pressReadyButton();
+
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerScreenStack.CurrentScreen).CurrentSubScreen;
-
- ((MultiplayerMatchSubScreen)currentSubScreen).SelectBeatmap();
+ ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true);
@@ -593,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)
@@ -666,7 +656,173 @@ 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", () => (multiplayerScreenStack.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true);
+
+ AddStep("press back button and exit", () =>
+ {
+ multiplayerScreenStack.OnBackButton();
+ multiplayerScreenStack.Exit();
+ });
+
+ AddUntilStep("wait for return to match subscreen", () => multiplayerScreenStack.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", () => (multiplayerScreenStack.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", () =>
+ {
+ multiplayerScreenStack.OnBackButton();
+ multiplayerScreenStack.Exit();
+ });
+
+ AddUntilStep("wait for return to match subscreen", () => multiplayerScreenStack.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", () => multiplayerScreenStack.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", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent());
+ AddUntilStep("queue is empty", () => this.ChildrenOfType().Single().Items.Count == 0);
+ }
+
+ private void enterGameplay()
+ {
+ pressReadyButton();
+ pressReadyButton();
+ AddUntilStep("wait for player", () => multiplayerScreenStack.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)
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index a5229702a8..d671673d3c 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -179,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/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/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/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/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
index 9c65b2dc51..14f32df653 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
@@ -393,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/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..25ca1299ef 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);
});
@@ -230,12 +241,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 f246560c82..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;
@@ -13,6 +14,7 @@ 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
@@ -22,13 +24,18 @@ namespace osu.Game.Tests.Visual.Ranking
[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/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 0494d1de3c..be390742ea 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -835,12 +835,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());
diff --git a/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs b/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs
index 7f1171db1f..370f3bd0ae 100644
--- a/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs
+++ b/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs
@@ -61,7 +61,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/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/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
index d93ac841ab..1e24501426 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
public const float TRANSITION_DURATION = 400;
public const float CORNER_RADIUS = 10;
- public Bindable Expanded { get; } = new BindableBool();
+ public IBindable Expanded { get; }
private const float width = 408;
private const float height = 100;
@@ -64,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);
@@ -282,15 +284,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
Hovered = _ =>
{
- content.ScheduleShow();
+ content.ExpandAfterDelay();
return false;
},
Unhovered = _ =>
{
- // This hide should only trigger if the expanded content has not shown yet.
- // ie. if the user has not shown intent to want to see it (quickly moved over the info row area).
+ // 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.ScheduleHide();
+ content.CancelExpand();
}
}
}
@@ -366,8 +368,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
protected override void OnHoverLost(HoverLostEvent e)
{
- content.ScheduleHide();
-
updateState();
base.OnHoverLost(e);
}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs
index 681f09c658..286e03e700 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs
@@ -31,7 +31,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards
set => dropdownScroll.Child = value;
}
- public Bindable Expanded { get; } = new BindableBool();
+ public IBindable Expanded => expanded;
+
+ private readonly BindableBool expanded = new BindableBool();
private readonly Box background;
private readonly Container content;
@@ -54,7 +56,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
AutoSizeAxes = Axes.Y,
CornerRadius = BeatmapCard.CORNER_RADIUS,
Masking = true,
- Unhovered = _ => checkForHide(),
+ Unhovered = _ => updateFromHoverChange(),
Children = new Drawable[]
{
background = new Box
@@ -76,10 +78,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Alpha = 0,
Hovered = _ =>
{
- keep();
+ updateFromHoverChange();
return true;
},
- Unhovered = _ => checkForHide(),
+ Unhovered = _ => updateFromHoverChange(),
Child = dropdownScroll = new ExpandedContentScrollContainer
{
RelativeSizeAxes = Axes.X,
@@ -119,51 +121,20 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private ScheduledDelegate? scheduledExpandedChange;
- public void ScheduleShow()
- {
- scheduledExpandedChange?.Cancel();
- if (Expanded.Disabled || Expanded.Value)
- return;
+ public void ExpandAfterDelay() => queueExpandedStateChange(true, 100);
- scheduledExpandedChange = Scheduler.AddDelayed(() =>
- {
- if (!Expanded.Disabled)
- Expanded.Value = true;
- }, 100);
- }
+ public void CancelExpand() => scheduledExpandedChange?.Cancel();
- public void ScheduleHide()
- {
- scheduledExpandedChange?.Cancel();
- if (Expanded.Disabled || !Expanded.Value)
- return;
+ private void updateFromHoverChange() =>
+ queueExpandedStateChange(content.IsHovered || dropdownContent.IsHovered, 100);
- scheduledExpandedChange = Scheduler.AddDelayed(() =>
- {
- if (!Expanded.Disabled)
- Expanded.Value = false;
- }, 500);
- }
-
- private void checkForHide()
- {
- if (Expanded.Disabled)
- return;
-
- if (content.IsHovered || dropdownContent.IsHovered)
- return;
-
- scheduledExpandedChange?.Cancel();
- Expanded.Value = false;
- }
-
- private void keep()
+ private void queueExpandedStateChange(bool newState, int delay = 0)
{
if (Expanded.Disabled)
return;
scheduledExpandedChange?.Cancel();
- Expanded.Value = true;
+ scheduledExpandedChange = Scheduler.AddDelayed(() => expanded.Value = newState, delay);
}
private void updateState()
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
index f47e1a7eab..8d3c606d76 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
@@ -298,7 +298,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
Hovered = _ =>
{
- content.ScheduleShow();
+ content.ExpandAfterDelay();
return false;
},
Unhovered = _ =>
@@ -306,7 +306,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
// This hide should only trigger if the expanded content has not shown yet.
// ie. if the user has not shown intent to want to see it (quickly moved over the info row area).
if (!Expanded.Value)
- content.ScheduleHide();
+ content.CancelExpand();
}
}
}
@@ -384,8 +384,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
protected override void OnHoverLost(HoverLostEvent e)
{
- content.ScheduleHide();
-
updateState();
base.OnHoverLost(e);
}
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/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs
index 13d362e0be..7cd9ae2885 100644
--- a/osu.Game/Database/OsuDbContext.cs
+++ b/osu.Game/Database/OsuDbContext.cs
@@ -147,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/RealmLive.cs b/osu.Game/Database/RealmLive.cs
index c376d5d503..90b8814c24 100644
--- a/osu.Game/Database/RealmLive.cs
+++ b/osu.Game/Database/RealmLive.cs
@@ -24,13 +24,17 @@ namespace osu.Game.Database
///
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;
+ this.realmFactory = realmFactory;
ID = data.ID;
}
@@ -47,7 +51,7 @@ namespace osu.Game.Database
return;
}
- using (var realm = Realm.GetInstance(data.Realm.Config))
+ using (var realm = realmFactory.CreateContext())
perform(realm.Find(ID));
}
@@ -58,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 (!IsManaged)
return perform(data);
- using (var realm = Realm.GetInstance(data.Realm.Config))
+ using (var realm = realmFactory.CreateContext())
return perform(realm.Find(ID));
}
@@ -74,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 =>
{
@@ -94,11 +98,7 @@ namespace osu.Game.Database
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads");
- // When using Value, we rely on garbage collection for the realm instance used to retrieve the instance.
- // As we are sure that this is on the update thread, there should always be an open and constantly refreshing realm instance to ensure file size growth is a non-issue.
- var realm = Realm.GetInstance(data.Realm.Config);
-
- return realm.Find(ID);
+ return realmFactory.Context.Find(ID);
}
}
diff --git a/osu.Game/Database/RealmLiveUnmanaged.cs b/osu.Game/Database/RealmLiveUnmanaged.cs
new file mode 100644
index 0000000000..5a69898206
--- /dev/null
+++ b/osu.Game/Database/RealmLiveUnmanaged.cs
@@ -0,0 +1,44 @@
+// 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 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 b38e21453c..e5177823ba 100644
--- a/osu.Game/Database/RealmObjectExtensions.cs
+++ b/osu.Game/Database/RealmObjectExtensions.cs
@@ -53,16 +53,28 @@ 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);
}
///
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/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/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/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/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 644c2e2a99..14eec8b388 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -111,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/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 7e874495c8..903aaa89e3 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -95,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.
///
@@ -162,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(item => createPlaylistItem(item, item.ID == joinedRoom.Settings.PlaylistItemId))).ConfigureAwait(false);
-
// Populate users.
Debug.Assert(joinedRoom.Users != null);
await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)).ConfigureAwait(false);
@@ -176,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);
@@ -219,7 +214,6 @@ namespace osu.Game.Online.Multiplayer
{
APIRoom = null;
Room = null;
- CurrentMatchPlayingItem.Value = null;
PlayingUserIds.Clear();
RoomUpdated?.Invoke();
@@ -333,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)
@@ -473,28 +473,7 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(APIRoom != null);
Debug.Assert(Room != null);
- Scheduler.Add(() =>
- {
- // ensure the new selected item is populated immediately.
- var playlistItem = APIRoom.Playlist.Single(p => p.ID == newSettings.PlaylistItemId);
-
- if (playlistItem != null)
- {
- GetAPIBeatmap(playlistItem.BeatmapID).ContinueWith(b =>
- {
- // Should be called outside of the `Scheduler` logic (and specifically accessing `Exception`) to suppress an exception from firing outwards.
- bool success = b.Exception == null;
-
- Scheduler.Add(() =>
- {
- if (success)
- playlistItem.Beatmap.Value = b.Result;
-
- updateLocalRoomSettings(newSettings);
- });
- });
- }
- });
+ Scheduler.Add(() => updateLocalRoomSettings(newSettings));
return Task.CompletedTask;
}
@@ -649,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, true).ConfigureAwait(false);
+ return Task.CompletedTask;
Scheduler.Add(() =>
{
@@ -664,11 +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)
@@ -693,12 +672,10 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask;
}
- public async Task PlaylistItemChanged(MultiplayerPlaylistItem item)
+ public Task PlaylistItemChanged(MultiplayerPlaylistItem item)
{
if (Room == null)
- return;
-
- var playlistItem = await createPlaylistItem(item, true).ConfigureAwait(false);
+ return Task.CompletedTask;
Scheduler.Add(() =>
{
@@ -711,15 +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;
}
///
@@ -748,12 +723,11 @@ namespace osu.Game.Online.Multiplayer
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, bool populateBeatmapImmediately)
+ private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item)
{
var ruleset = Rulesets.GetRuleset(item.RulesetID);
@@ -775,9 +749,6 @@ namespace osu.Game.Online.Multiplayer
playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance)));
playlistItem.AllowedMods.AddRange(item.AllowedMods.Select(m => m.ToMod(rulesetInstance)));
- if (populateBeatmapImmediately)
- playlistItem.Beatmap.Value = await GetAPIBeatmap(item.BeatmapID).ConfigureAwait(false);
-
return playlistItem;
}
@@ -787,7 +758,7 @@ namespace osu.Game.Online.Multiplayer
/// The beatmap ID.
/// A token to cancel the request.
/// The retrieval task.
- protected abstract Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default);
+ 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 41687b54b0..3794bec228 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -154,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)
@@ -162,7 +170,23 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.AddPlaylistItem), item);
}
- protected override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default)
+ public override Task EditPlaylistItem(MultiplayerPlaylistItem item)
+ {
+ if (!IsConnected.Value)
+ return Task.CompletedTask;
+
+ return connection.InvokeAsync(nameof(IMultiplayerServer.EditPlaylistItem), item);
+ }
+
+ public override Task RemovePlaylistItem(long playlistItemId)
+ {
+ if (!IsConnected.Value)
+ return Task.CompletedTask;
+
+ return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId);
+ }
+
+ public override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default)
{
return beatmapLookupCache.GetBeatmapAsync(beatmapId, cancellationToken);
}
diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs
index cee6d8fe41..8ec073ff1e 100644
--- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs
+++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs
@@ -62,6 +62,7 @@ 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;
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/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/OsuGame.cs b/osu.Game/OsuGame.cs
index 047c3b4225..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; }
@@ -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)
{
@@ -255,10 +255,10 @@ namespace osu.Game
if (skinInfo == null)
{
if (guid == SkinInfo.CLASSIC_SKIN)
- skinInfo = DefaultLegacySkin.CreateInfo().ToLive();
+ skinInfo = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged();
}
- SkinManager.CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.CreateInfo().ToLive();
+ SkinManager.CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.CreateInfo().ToLiveUnmanaged();
};
configSkin.TriggerChange();
@@ -487,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);
@@ -698,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,
@@ -801,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
{
@@ -1149,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/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/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/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 be9d3cd794..6b3696ced9 100644
--- a/osu.Game/Overlays/OnScreenDisplay.cs
+++ b/osu.Game/Overlays/OnScreenDisplay.cs
@@ -101,7 +101,7 @@ namespace osu.Game.Overlays
DisplayTemporarily(box);
});
- private void displayTrackedSettingChange(SettingDescription description) => 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/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/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index b1582d7bee..0fa6d78d4b 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -32,14 +32,14 @@ namespace osu.Game.Overlays.Settings.Sections
Icon = FontAwesome.Solid.PaintBrush
};
- private readonly Bindable> dropdownBindable = new Bindable> { Default = DefaultSkin.CreateInfo().ToLive() };
+ private readonly Bindable> dropdownBindable = new Bindable> { Default = DefaultSkin.CreateInfo().ToLiveUnmanaged() };
private readonly Bindable configBindable = new Bindable();
private static readonly ILive random_skin_info = new SkinInfo
{
ID = SkinInfo.RANDOM_SKIN,
Name = "",
- }.ToLive();
+ }.ToLiveUnmanaged();
private List> skinItems;
@@ -133,7 +133,7 @@ namespace osu.Game.Overlays.Settings.Sections
{
int protectedCount = realmSkins.Count(s => s.Protected);
- skinItems = realmSkins.ToLive();
+ skinItems = realmSkins.ToLive(realmFactory);
skinItems.Insert(protectedCount, random_skin_info);
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/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/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/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index 5dc88d7644..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
@@ -136,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; }
@@ -231,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
@@ -256,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/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/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 02565c6ebe..238aa4059d 100644
--- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
@@ -7,6 +7,7 @@ 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.Online.API;
@@ -107,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))
@@ -136,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/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs
index f2d31c8e67..57bb4253cb 100644
--- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs
+++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs
@@ -1,9 +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.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;
@@ -12,36 +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 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;
+
+ 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 =>
@@ -54,23 +154,20 @@ namespace osu.Game.Screens.OnlinePlay
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);
- }
+ 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 c291bddeeb..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;
@@ -25,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;
@@ -39,12 +40,38 @@ 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;
@@ -53,14 +80,11 @@ namespace osu.Game.Screens.OnlinePlay
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; }
@@ -68,38 +92,25 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved]
private UserLookupCache userLookupCache { get; set; }
+ [CanBeNull]
+ [Resolved(CanBeNull = true)]
+ private MultiplayerClient multiplayerClient { get; set; }
+
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; }
- private PanelBackground panelBackground;
+ protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model;
- private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both };
-
- private readonly bool allowEdit;
- private readonly bool allowSelection;
- private readonly bool showItemOwner;
-
- private FillFlowContainer mainFillFlow;
-
- protected override bool ShouldBeConsideredForInput(Drawable child) => allowEdit || !allowSelection || SelectedItem.Value == Model;
-
- public DrawableRoomPlaylistItem(PlaylistItem item, bool allowEdit, bool allowSelection, bool showItemOwner)
+ 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);
}
@@ -107,9 +118,6 @@ namespace osu.Game.Screens.OnlinePlay
[BackgroundDependencyLoader]
private void load()
{
- if (!allowEdit)
- HandleColour = HandleColour.Opacity(0);
-
maskingContainer.BorderColour = colours.Yellow;
}
@@ -155,7 +163,15 @@ namespace osu.Game.Screens.OnlinePlay
if (Item.Beatmap.Value == null)
{
- var foundBeatmap = await beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ConfigureAwait(false);
+ 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);
}
}
@@ -169,6 +185,88 @@ namespace osu.Game.Screens.OnlinePlay
refresh();
}
+ ///
+ /// 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()
{
if (!valid.Value)
@@ -178,7 +276,7 @@ namespace osu.Game.Screens.OnlinePlay
}
if (Item.Beatmap.Value != null)
- difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(ICON_HEIGHT) };
+ difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) };
else
difficultyIconContainer.Clear();
@@ -208,7 +306,7 @@ namespace osu.Game.Screens.OnlinePlay
modDisplay.Current.Value = requiredMods.ToArray();
buttonsFlow.Clear();
- buttonsFlow.ChildrenEnumerable = CreateButtons();
+ buttonsFlow.ChildrenEnumerable = createButtons();
difficultyIconContainer.FadeInFromZero(500, Easing.OutQuint);
mainFillFlow.FadeInFromZero(500, Easing.OutQuint);
@@ -322,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;
@@ -332,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
},
}
}
@@ -345,38 +443,53 @@ namespace osu.Game.Screens.OnlinePlay
};
}
- protected virtual IEnumerable CreateButtons() =>
- new[]
+ private IEnumerable createButtons() => new[]
+ {
+ showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie)
{
- Item.Beatmap.Value == null ? Empty() : 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/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
index 184ac2c563..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)
@@ -374,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;
@@ -443,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 39d60a0b05..7f1db733b3 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
@@ -248,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 874113d859..06959d942f 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs
@@ -63,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();
@@ -104,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
index d708b39898..32d355d149 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs
@@ -16,8 +16,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
public class MultiplayerHistoryList : DrawableRoomPlaylist
{
public MultiplayerHistoryList()
- : base(false, false, true)
{
+ ShowItemOwners = true;
}
protected override FillFlowContainer> CreateListFillFlowContainer() => new HistoryFillFlowContainer
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs
index c3245b550f..7b90532cce 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -19,6 +20,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
{
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;
@@ -46,7 +52,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
queueList = new MultiplayerQueueList
{
RelativeSizeAxes = Axes.Both,
- SelectedItem = { BindTarget = SelectedItem }
+ SelectedItem = { BindTarget = SelectedItem },
+ RequestEdit = item => RequestEdit?.Invoke(item)
},
historyList = new MultiplayerHistoryList
{
@@ -114,7 +121,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
private void addItemToLists(MultiplayerPlaylistItem item)
{
- var apiItem = Playlist.Single(i => i.ID == item.ID);
+ 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);
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
index 1b1b66273f..3e0f663d42 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
@@ -7,6 +7,9 @@ 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;
@@ -18,8 +21,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
public class MultiplayerQueueList : DrawableRoomPlaylist
{
public MultiplayerQueueList()
- : base(false, false, true)
{
+ ShowItemOwners = true;
}
protected override FillFlowContainer> CreateListFillFlowContainer() => new QueueFillFlowContainer
@@ -27,6 +30,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
Spacing = new Vector2(0, 2)
};
+ protected override DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new QueuePlaylistItem(item);
+
private class QueueFillFlowContainer : FillFlowContainer>
{
[Resolved(typeof(Room), nameof(Room.Playlist))]
@@ -40,5 +45,44 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
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 3a25bd7b06..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;
@@ -44,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; }
@@ -57,6 +55,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[CanBeNull]
private IDisposable readyClickOperation;
+ private AddItemButton addItemButton;
+
public MultiplayerMatchSubScreen(Room room)
: base(room)
{
@@ -68,8 +68,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.LoadComplete();
- SelectedItem.BindTo(client.CurrentMatchPlayingItem);
-
BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true);
UserMods.BindValueChanged(onUserModsChanged);
@@ -134,12 +132,12 @@ 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 = SelectBeatmap,
- Alpha = 0
+ Text = "Add item",
+ Action = () => OpenSongSelection()
},
},
null,
@@ -147,7 +145,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
new MultiplayerPlaylist
{
- RelativeSizeAxes = Axes.Both
+ RelativeSizeAxes = Axes.Both,
+ RequestEdit = item => OpenSongSelection(item.ID)
}
},
new[]
@@ -220,12 +219,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
};
- internal void SelectBeatmap()
+ ///
+ /// 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));
+ this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit));
}
protected override Drawable CreateFooter() => new MultiplayerMatchFooter
@@ -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/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
index 9ac64add9a..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;
@@ -226,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/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 . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Extensions.IEnumerableExtensions;
+
+namespace osu.Game.Screens.OnlinePlay.Playlists
+{
+ ///
+ /// A which is displayed during the setup stage of a playlists room.
+ ///
+ public class PlaylistsRoomSettingsPlaylist : DrawableRoomPlaylist
+ {
+ public PlaylistsRoomSettingsPlaylist()
+ {
+ AllowReordering = true;
+ AllowDeletion = true;
+
+ RequestDeletion = item =>
+ {
+ var nextItem = Items.GetNext(item);
+
+ Items.Remove(item);
+
+ SelectedItem.Value = nextItem ?? Items.LastOrDefault();
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs
index 7e045802f7..4114a5e9a0 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs
@@ -88,12 +88,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new Drawable[] { new OverlinedPlaylistHeader(), },
new Drawable[]
{
- new DrawableRoomPlaylistWithResults
+ new DrawableRoomPlaylist
{
RelativeSizeAxes = Axes.Both,
Items = { BindTarget = Room.Playlist },
SelectedItem = { BindTarget = SelectedItem },
- RequestShowResults = item =>
+ AllowSelection = true,
+ AllowShowingResults = true,
+ RequestResults = item =>
{
Debug.Assert(RoomId.Value != null);
ParentScreen?.Push(new PlaylistsResultsScreen(null, RoomId.Value.Value, item, false));
diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs
index 193e1e4129..cfbfdc9966 100644
--- a/osu.Game/Screens/Play/FailAnimation.cs
+++ b/osu.Game/Screens/Play/FailAnimation.cs
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Game.Rulesets.UI;
using System;
using System.Collections.Generic;
+using JetBrains.Annotations;
using ManagedBass.Fx;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
@@ -18,6 +19,7 @@ using osu.Framework.Utils;
using osu.Game.Audio.Effects;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
@@ -58,6 +60,12 @@ namespace osu.Game.Screens.Play
RelativeSizeAxes = Axes.Both,
};
+ ///
+ /// The player screen background, used to adjust appearance on failing.
+ ///
+ [CanBeNull]
+ public BackgroundScreen Background { private get; set; }
+
public FailAnimation(DrawableRuleset drawableRuleset)
{
this.drawableRuleset = drawableRuleset;
@@ -136,6 +144,9 @@ namespace osu.Game.Screens.Play
Content.ScaleTo(0.85f, duration, Easing.OutQuart);
Content.RotateTo(1, duration, Easing.OutQuart);
Content.FadeColour(Color4.Gray, duration);
+
+ // Will be restored by `ApplyToBackground` logic in `SongSelect`.
+ Background?.FadeColour(OsuColour.Gray(0.3f), 60);
}
public void RemoveFilters(bool resetTrackFrequency = true)
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index a0e9428cff..745e1f9e7c 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -921,6 +921,8 @@ namespace osu.Game.Screens.Play
b.IsBreakTime.BindTo(breakTracker.IsBreakTime);
b.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
+
+ failAnimationLayer.Background = b;
});
HUDOverlay.IsBreakTime.BindTo(breakTracker.IsBreakTime);
@@ -1031,13 +1033,13 @@ namespace osu.Game.Screens.Play
//
// Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint
// conflicts across various systems (ie. solo and multiplayer).
- long? onlineScoreId = score.ScoreInfo.OnlineScoreID;
- score.ScoreInfo.OnlineScoreID = null;
+ long? onlineScoreId = score.ScoreInfo.OnlineID;
+ score.ScoreInfo.OnlineID = -1;
await scoreManager.Import(score.ScoreInfo, replayReader).ConfigureAwait(false);
// ... And restore the online ID for other processes to handle correctly (e.g. de-duplication for the results screen).
- score.ScoreInfo.OnlineScoreID = onlineScoreId;
+ score.ScoreInfo.OnlineID = onlineScoreId;
}
///
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 57db411571..60843acb4f 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -468,12 +468,14 @@ namespace osu.Game.Screens.Play
private int restartCount;
+ private const double volume_requirement = 0.05;
+
private void showMuteWarningIfNeeded()
{
if (!muteWarningShownOnce.Value)
{
// Checks if the notification has not been shown yet and also if master volume is muted, track/music volume is muted or if the whole game is muted.
- if (volumeOverlay?.IsMuted.Value == true || audioManager.Volume.Value <= audioManager.Volume.MinValue || audioManager.VolumeTrack.Value <= audioManager.VolumeTrack.MinValue)
+ if (volumeOverlay?.IsMuted.Value == true || audioManager.Volume.Value <= volume_requirement || audioManager.VolumeTrack.Value <= volume_requirement)
{
notificationOverlay?.Post(new MutedNotification());
muteWarningShownOnce.Value = true;
@@ -487,7 +489,7 @@ namespace osu.Game.Screens.Play
public MutedNotification()
{
- Text = "Your music volume is set to 0%! Click here to restore it.";
+ Text = "Your game volume is too low to hear anything! Click here to restore it.";
}
[BackgroundDependencyLoader]
@@ -501,8 +503,12 @@ namespace osu.Game.Screens.Play
notificationOverlay.Hide();
volumeOverlay.IsMuted.Value = false;
- audioManager.Volume.SetDefault();
- audioManager.VolumeTrack.SetDefault();
+
+ // Check values before resetting, as the user may have only had mute enabled, in which case we might not need to adjust volumes.
+ if (audioManager.Volume.Value <= volume_requirement)
+ audioManager.Volume.SetDefault();
+ if (audioManager.VolumeTrack.Value <= volume_requirement)
+ audioManager.VolumeTrack.SetDefault();
return true;
};
diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs
index 3918dbe8fc..ba5663bfa3 100644
--- a/osu.Game/Screens/Play/SoloSpectator.cs
+++ b/osu.Game/Screens/Play/SoloSpectator.cs
@@ -228,10 +228,7 @@ namespace osu.Game.Screens.Play
onlineBeatmapRequest.Success += beatmapSet => Schedule(() =>
{
this.beatmapSet = beatmapSet;
- beatmapPanelContainer.Child = new BeatmapCard(this.beatmapSet)
- {
- Expanded = { Disabled = true }
- };
+ beatmapPanelContainer.Child = new BeatmapCard(this.beatmapSet, allowExpansion: false);
checkForAutomaticDownload();
});
diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs
index f6a89e7fa9..d42643c416 100644
--- a/osu.Game/Screens/Play/SpectatorPlayer.cs
+++ b/osu.Game/Screens/Play/SpectatorPlayer.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Screens.Play
private void userSentFrames(int userId, FrameDataBundle bundle)
{
- if (userId != score.ScoreInfo.User.Id)
+ if (userId != score.ScoreInfo.User.OnlineID)
return;
if (!LoadedBeatmapSuccessfully)
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index c07cfa9c4d..c613167908 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -156,7 +156,7 @@ namespace osu.Game.Screens.Play
request.Success += s =>
{
- score.ScoreInfo.OnlineScoreID = s.ID;
+ score.ScoreInfo.OnlineID = s.ID;
score.ScoreInfo.Position = s.Position;
scoreSubmissionSource.SetResult(true);
diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs
index 0935ee7fb2..beff509dc6 100644
--- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs
+++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs
@@ -2,36 +2,42 @@
// 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.Graphics;
using osu.Game.Graphics.Sprites;
-using osu.Game.Scoring;
namespace osu.Game.Screens.Ranking.Contracted
{
public class ContractedPanelTopContent : CompositeDrawable
{
- private readonly ScoreInfo score;
+ public readonly Bindable ScorePosition = new Bindable();
- public ContractedPanelTopContent(ScoreInfo score)
+ private OsuSpriteText text;
+
+ public ContractedPanelTopContent()
{
- this.score = score;
-
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
- InternalChild = new OsuSpriteText
+ InternalChild = text = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Y = 6,
- Text = score.Position != null ? $"#{score.Position}" : string.Empty,
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold)
};
}
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ ScorePosition.BindValueChanged(pos => text.Text = pos.NewValue != null ? $"#{pos.NewValue}" : string.Empty, true);
+ }
}
}
diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs
index 6ddecf8297..bc6eb9e366 100644
--- a/osu.Game/Screens/Ranking/ScorePanel.cs
+++ b/osu.Game/Screens/Ranking/ScorePanel.cs
@@ -4,6 +4,7 @@
using System;
using osu.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -78,6 +79,11 @@ namespace osu.Game.Screens.Ranking
public event Action StateChanged;
+ ///
+ /// The position of the score in the rankings.
+ ///
+ public readonly Bindable ScorePosition = new Bindable();
+
///
/// An action to be invoked if this is clicked while in an expanded state.
///
@@ -103,6 +109,8 @@ namespace osu.Game.Screens.Ranking
{
Score = score;
displayWithFlair = isNewLocalScore;
+
+ ScorePosition.Value = score.Position;
}
[BackgroundDependencyLoader]
@@ -211,8 +219,8 @@ namespace osu.Game.Screens.Ranking
topLayerBackground.FadeColour(expanded_top_layer_colour, RESIZE_DURATION, Easing.OutQuint);
middleLayerBackground.FadeColour(expanded_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint);
- topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0));
- middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, displayWithFlair).With(d => d.Alpha = 0));
+ topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User) { Alpha = 0 });
+ middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, displayWithFlair) { Alpha = 0 });
// only the first expanded display should happen with flair.
displayWithFlair = false;
@@ -224,8 +232,13 @@ namespace osu.Game.Screens.Ranking
topLayerBackground.FadeColour(contracted_top_layer_colour, RESIZE_DURATION, Easing.OutQuint);
middleLayerBackground.FadeColour(contracted_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint);
- topLayerContentContainer.Add(topLayerContent = new ContractedPanelTopContent(Score).With(d => d.Alpha = 0));
- middleLayerContentContainer.Add(middleLayerContent = new ContractedPanelMiddleContent(Score).With(d => d.Alpha = 0));
+ topLayerContentContainer.Add(topLayerContent = new ContractedPanelTopContent
+ {
+ ScorePosition = { BindTarget = ScorePosition },
+ Alpha = 0
+ });
+
+ middleLayerContentContainer.Add(middleLayerContent = new ContractedPanelMiddleContent(Score) { Alpha = 0 });
break;
}
diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs
index 22be91b974..f3de48dcf0 100644
--- a/osu.Game/Screens/Ranking/ScorePanelList.cs
+++ b/osu.Game/Screens/Ranking/ScorePanelList.cs
@@ -341,7 +341,7 @@ namespace osu.Game.Screens.Ranking
private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType()
.OrderByDescending(GetLayoutPosition)
- .ThenBy(s => s.Panel.Score.OnlineScoreID);
+ .ThenBy(s => s.Panel.Score.OnlineID);
}
private class Scroll : OsuScrollContainer
diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
index 929bda6508..afebc728b4 100644
--- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Screens.Ranking
return null;
getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset);
- getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets, Beatmap.Value.BeatmapInfo)));
+ getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineID != Score.OnlineID).Select(s => s.CreateScoreInfo(rulesets, Beatmap.Value.BeatmapInfo)));
return getScoreRequest;
}
diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index e4cf9bd868..6791565828 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -163,7 +163,7 @@ namespace osu.Game.Screens.Select
private FillFlowContainer infoLabelContainer;
private Container bpmLabelContainer;
- private readonly WorkingBeatmap beatmap;
+ private readonly WorkingBeatmap working;
private readonly RulesetInfo ruleset;
[Resolved]
@@ -171,10 +171,10 @@ namespace osu.Game.Screens.Select
private ModSettingChangeTracker settingChangeTracker;
- public WedgeInfoText(WorkingBeatmap beatmap, RulesetInfo userRuleset)
+ public WedgeInfoText(WorkingBeatmap working, RulesetInfo userRuleset)
{
- this.beatmap = beatmap;
- ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset;
+ this.working = working;
+ ruleset = userRuleset ?? working.BeatmapInfo.Ruleset;
}
private CancellationTokenSource cancellationSource;
@@ -183,8 +183,8 @@ namespace osu.Game.Screens.Select
[BackgroundDependencyLoader]
private void load(OsuColour colours, LocalisationManager localisation, BeatmapDifficultyCache difficultyCache)
{
- var beatmapInfo = beatmap.BeatmapInfo;
- var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
+ var beatmapInfo = working.BeatmapInfo;
+ var metadata = beatmapInfo.Metadata ?? working.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
RelativeSizeAxes = Axes.Both;
@@ -330,36 +330,9 @@ namespace osu.Game.Screens.Select
addInfoLabels();
}
- private void setMetadata(string source)
+ protected override void LoadComplete()
{
- ArtistLabel.Text = artistBinding.Value;
- TitleLabel.Text = string.IsNullOrEmpty(source) ? titleBinding.Value : source + " — " + titleBinding.Value;
- }
-
- private void addInfoLabels()
- {
- if (beatmap.Beatmap?.HitObjects?.Any() != true)
- return;
-
- infoLabelContainer.Children = new Drawable[]
- {
- new InfoLabel(new BeatmapStatistic
- {
- Name = "Length",
- CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
- Content = beatmap.BeatmapInfo.Length.ToFormattedDuration().ToString(),
- }),
- bpmLabelContainer = new Container
- {
- AutoSizeAxes = Axes.Both,
- },
- new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Spacing = new Vector2(20, 0),
- Children = getRulesetInfoLabels()
- }
- };
+ base.LoadComplete();
mods.BindValueChanged(m =>
{
@@ -372,6 +345,38 @@ namespace osu.Game.Screens.Select
}, true);
}
+ private void setMetadata(string source)
+ {
+ ArtistLabel.Text = artistBinding.Value;
+ TitleLabel.Text = string.IsNullOrEmpty(source) ? titleBinding.Value : source + " — " + titleBinding.Value;
+ }
+
+ private void addInfoLabels()
+ {
+ if (working.Beatmap?.HitObjects?.Any() != true)
+ return;
+
+ infoLabelContainer.Children = new Drawable[]
+ {
+ new InfoLabel(new BeatmapStatistic
+ {
+ Name = "Length",
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
+ Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(),
+ }),
+ bpmLabelContainer = new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Spacing = new Vector2(20, 0),
+ Children = getRulesetInfoLabels()
+ }
+ };
+ }
+
private InfoLabel[] getRulesetInfoLabels()
{
try
@@ -381,12 +386,12 @@ namespace osu.Game.Screens.Select
try
{
// Try to get the beatmap with the user's ruleset
- playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty());
+ playableBeatmap = working.GetPlayableBeatmap(ruleset, Array.Empty());
}
catch (BeatmapInvalidForRulesetException)
{
// Can't be converted to the user's ruleset, so use the beatmap's own ruleset
- playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty());
+ playableBeatmap = working.GetPlayableBeatmap(working.BeatmapInfo.Ruleset, Array.Empty());
}
return playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)).ToArray();
@@ -401,8 +406,9 @@ namespace osu.Game.Screens.Select
private void refreshBPMLabel()
{
- var b = beatmap.Beatmap;
- if (b == null)
+ var beatmap = working.Beatmap;
+
+ if (beatmap == null || bpmLabelContainer == null)
return;
// this doesn't consider mods which apply variable rates, yet.
@@ -410,9 +416,9 @@ namespace osu.Game.Screens.Select
foreach (var mod in mods.Value.OfType())
rate = mod.ApplyToRate(0, rate);
- double bpmMax = b.ControlPointInfo.BPMMaximum * rate;
- double bpmMin = b.ControlPointInfo.BPMMinimum * rate;
- double mostCommonBPM = 60000 / b.GetMostCommonBeatLength() * rate;
+ double bpmMax = beatmap.ControlPointInfo.BPMMaximum * rate;
+ double bpmMin = beatmap.ControlPointInfo.BPMMinimum * rate;
+ double mostCommonBPM = 60000 / beatmap.GetMostCommonBeatLength() * rate;
string labelText = Precision.AlmostEquals(bpmMin, bpmMax)
? $"{bpmMin:0}"
diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs
index 54fc2340f1..d606d94b97 100644
--- a/osu.Game/Skinning/Skin.cs
+++ b/osu.Game/Skinning/Skin.cs
@@ -43,7 +43,11 @@ namespace osu.Game.Skinning
protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null)
{
- SkinInfo = skin.ToLive();
+ SkinInfo = resources?.RealmContextFactory != null
+ ? skin.ToLive(resources.RealmContextFactory)
+ // This path should only be used in some tests.
+ : skin.ToLiveUnmanaged();
+
this.resources = resources;
configurationStream ??= getConfigurationStream();
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 5134632fb1..bb2f0a37b4 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -47,9 +47,9 @@ namespace osu.Game.Skinning
public readonly Bindable CurrentSkin = new Bindable();
- public readonly Bindable> CurrentSkinInfo = new Bindable>(Skinning.DefaultSkin.CreateInfo().ToLive())
+ public readonly Bindable> CurrentSkinInfo = new Bindable>(Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged())
{
- Default = Skinning.DefaultSkin.CreateInfo().ToLive()
+ Default = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()
};
private readonly SkinModelManager skinModelManager;
@@ -119,13 +119,13 @@ namespace osu.Game.Skinning
if (randomChoices.Length == 0)
{
- CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLive();
+ CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged();
return;
}
var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length));
- CurrentSkinInfo.Value = chosen.ToLive();
+ CurrentSkinInfo.Value = chosen.ToLive(contextFactory);
}
}
@@ -182,7 +182,7 @@ namespace osu.Game.Skinning
public ILive Query(Expression> query)
{
using (var context = contextFactory.CreateContext())
- return context.All().FirstOrDefault(query)?.ToLive();
+ return context.All().FirstOrDefault(query)?.ToLive(contextFactory);
}
public event Action SourceChanged;
@@ -237,6 +237,7 @@ namespace osu.Game.Skinning
AudioManager IStorageResourceProvider.AudioManager => audio;
IResourceStore IStorageResourceProvider.Resources => resources;
IResourceStore IStorageResourceProvider.Files => userFiles;
+ RealmContextFactory IStorageResourceProvider.RealmContextFactory => contextFactory;
IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore);
#endregion
@@ -302,7 +303,7 @@ namespace osu.Game.Skinning
Guid currentUserSkin = CurrentSkinInfo.Value.ID;
if (items.Any(s => s.ID == currentUserSkin))
- scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLive());
+ scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged());
skinModelManager.Delete(items.ToList(), silent);
}
diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs
index 1681dad750..4aca079e2e 100644
--- a/osu.Game/Stores/RealmArchiveModelImporter.cs
+++ b/osu.Game/Stores/RealmArchiveModelImporter.cs
@@ -352,7 +352,7 @@ namespace osu.Game.Stores
transaction.Commit();
}
- return existing.ToLive();
+ return existing.ToLive(ContextFactory);
}
LogForModel(item, @"Found existing (optimised) but failed pre-check.");
@@ -387,7 +387,7 @@ namespace osu.Game.Stores
existing.DeletePending = false;
transaction.Commit();
- return existing.ToLive();
+ return existing.ToLive(ContextFactory);
}
LogForModel(item, @"Found existing but failed re-use check.");
@@ -416,7 +416,7 @@ namespace osu.Game.Stores
throw;
}
- return item.ToLive();
+ return item.ToLive(ContextFactory);
}
}
diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs
index 6fb2f5994b..ebd1a941a8 100644
--- a/osu.Game/Storyboards/StoryboardSprite.cs
+++ b/osu.Game/Storyboards/StoryboardSprite.cs
@@ -33,10 +33,8 @@ namespace osu.Game.Storyboards
foreach (var l in loops)
{
- if (!(l.EarliestDisplayedTime is double lEarliest))
- continue;
-
- earliestStartTime = Math.Min(earliestStartTime, lEarliest);
+ if (l.EarliestDisplayedTime is double loopEarliestDisplayTime)
+ earliestStartTime = Math.Min(earliestStartTime, l.LoopStartTime + loopEarliestDisplayTime);
}
if (earliestStartTime < double.MaxValue)
diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
index 31a2071249..f919edecf7 100644
--- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
+++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
@@ -14,6 +14,7 @@ using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
+using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Models;
using osu.Game.Rulesets;
@@ -118,6 +119,7 @@ namespace osu.Game.Tests.Beatmaps
public IResourceStore Files => userSkinResourceStore;
public new IResourceStore Resources => base.Resources;
public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null;
+ RealmContextFactory IStorageResourceProvider.RealmContextFactory => null;
#endregion
diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs
deleted file mode 100644
index a53cb0ae78..0000000000
--- a/osu.Game/Tests/TestScoreInfo.cs
+++ /dev/null
@@ -1,66 +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.Linq;
-using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Rulesets;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Scoring;
-using osu.Game.Scoring;
-using osu.Game.Tests.Beatmaps;
-
-namespace osu.Game.Tests
-{
- public class TestScoreInfo : ScoreInfo
- {
- public TestScoreInfo(RulesetInfo ruleset, bool excessMods = false)
- {
- User = new APIUser
- {
- Id = 2,
- Username = "peppy",
- CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
- };
-
- BeatmapInfo = new TestBeatmap(ruleset).BeatmapInfo;
- Ruleset = ruleset;
- RulesetID = ruleset.ID ?? 0;
-
- Mods = excessMods
- ? ruleset.CreateInstance().CreateAllMods().ToArray()
- : new Mod[] { new TestModHardRock(), new TestModDoubleTime() };
-
- TotalScore = 2845370;
- Accuracy = 0.95;
- MaxCombo = 999;
- Rank = ScoreRank.S;
- Date = DateTimeOffset.Now;
-
- Statistics[HitResult.Miss] = 1;
- Statistics[HitResult.Meh] = 50;
- Statistics[HitResult.Ok] = 100;
- Statistics[HitResult.Good] = 200;
- Statistics[HitResult.Great] = 300;
- Statistics[HitResult.Perfect] = 320;
- Statistics[HitResult.SmallTickHit] = 50;
- Statistics[HitResult.SmallTickMiss] = 25;
- Statistics[HitResult.LargeTickHit] = 100;
- Statistics[HitResult.LargeTickMiss] = 50;
- Statistics[HitResult.SmallBonus] = 10;
- Statistics[HitResult.SmallBonus] = 50;
-
- Position = 1;
- }
-
- private class TestModHardRock : ModHardRock
- {
- public override double ScoreMultiplier => 1;
- }
-
- private class TestModDoubleTime : ModDoubleTime
- {
- public override double ScoreMultiplier => 1;
- }
- }
-}
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index b3ea5bdc4a..4e0cfe405e 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -128,6 +128,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
case MultiplayerRoomState.WaitingForLoad:
if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
{
+ var loadedUsers = Room.Users.Where(u => u.State == MultiplayerUserState.Loaded).ToArray();
+
+ if (loadedUsers.Length == 0)
+ {
+ // all users have bailed from the load sequence. cancel the game start.
+ ChangeRoomState(MultiplayerRoomState.Open);
+ return;
+ }
+
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded))
ChangeUserState(u.UserID, MultiplayerUserState.Playing);
@@ -143,8 +152,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay))
ChangeUserState(u.UserID, MultiplayerUserState.Results);
- ChangeRoomState(MultiplayerRoomState.Open);
+ ChangeRoomState(MultiplayerRoomState.Open);
((IMultiplayerClient)this).ResultsReady();
FinishCurrentItem().Wait();
@@ -242,6 +251,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
public override Task ChangeState(MultiplayerUserState newState)
{
+ Debug.Assert(Room != null);
+
+ if (newState == MultiplayerUserState.Idle && LocalUser?.State == MultiplayerUserState.WaitingForLoad)
+ return Task.CompletedTask;
+
ChangeUserState(api.LocalUser.Value.Id, newState);
return Task.CompletedTask;
}
@@ -303,6 +317,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
return ((IMultiplayerClient)this).LoadRequested();
}
+ public override Task AbortGameplay()
+ {
+ Debug.Assert(Room != null);
+ Debug.Assert(LocalUser != null);
+
+ ChangeUserState(LocalUser.UserID, MultiplayerUserState.Idle);
+
+ return Task.CompletedTask;
+ }
+
public async Task AddUserPlaylistItem(int userId, MultiplayerPlaylistItem item)
{
Debug.Assert(Room != null);
@@ -314,32 +338,69 @@ namespace osu.Game.Tests.Visual.Multiplayer
item.OwnerID = userId;
- switch (Room.Settings.QueueMode)
- {
- case QueueMode.HostOnly:
- // In host-only mode, the current item is re-used.
- item.ID = currentItem.ID;
- item.PlaylistOrder = currentItem.PlaylistOrder;
-
- serverSidePlaylist[currentIndex] = item;
- await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false);
-
- // Note: Unlike the server, this is the easiest way to update the current item at this point.
- await updateCurrentItem(Room, false).ConfigureAwait(false);
- break;
-
- default:
- await addItem(item).ConfigureAwait(false);
-
- // The current item can change as a result of an item being added. For example, if all items earlier in the queue were expired.
- await updateCurrentItem(Room).ConfigureAwait(false);
- break;
- }
+ await addItem(item).ConfigureAwait(false);
+ await updateCurrentItem(Room).ConfigureAwait(false);
}
public override Task AddPlaylistItem(MultiplayerPlaylistItem item) => AddUserPlaylistItem(api.LocalUser.Value.OnlineID, item);
- protected override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default)
+ public async Task EditUserPlaylistItem(int userId, MultiplayerPlaylistItem item)
+ {
+ Debug.Assert(Room != null);
+ Debug.Assert(APIRoom != null);
+ Debug.Assert(currentItem != null);
+
+ item.OwnerID = userId;
+
+ var existingItem = serverSidePlaylist.SingleOrDefault(i => i.ID == item.ID);
+
+ if (existingItem == null)
+ throw new InvalidOperationException("Attempted to change an item that doesn't exist.");
+
+ if (existingItem.OwnerID != userId && Room.Host?.UserID != LocalUser?.UserID)
+ throw new InvalidOperationException("Attempted to change an item which is not owned by the user.");
+
+ if (existingItem.Expired)
+ throw new InvalidOperationException("Attempted to change an item which has already been played.");
+
+ // Ensure the playlist order doesn't change.
+ item.PlaylistOrder = existingItem.PlaylistOrder;
+
+ serverSidePlaylist[serverSidePlaylist.IndexOf(existingItem)] = item;
+
+ await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false);
+ }
+
+ public override Task EditPlaylistItem(MultiplayerPlaylistItem item) => EditUserPlaylistItem(api.LocalUser.Value.OnlineID, item);
+
+ public async Task RemoveUserPlaylistItem(int userId, long playlistItemId)
+ {
+ Debug.Assert(Room != null);
+ Debug.Assert(APIRoom != null);
+
+ var item = serverSidePlaylist.Find(i => i.ID == playlistItemId);
+
+ if (item == null)
+ throw new InvalidOperationException("Item does not exist in the room.");
+
+ if (item == currentItem)
+ throw new InvalidOperationException("The room's current item cannot be removed.");
+
+ if (item.OwnerID != userId)
+ throw new InvalidOperationException("Attempted to remove an item which is not owned by the user.");
+
+ if (item.Expired)
+ throw new InvalidOperationException("Attempted to remove an item which has already been played.");
+
+ serverSidePlaylist.Remove(item);
+ await ((IMultiplayerClient)this).PlaylistItemRemoved(playlistItemId).ConfigureAwait(false);
+
+ await updateCurrentItem(Room).ConfigureAwait(false);
+ }
+
+ public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, playlistItemId);
+
+ public override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default)
{
IBeatmapSetInfo? set = roomManager.ServerSideRooms.SelectMany(r => r.Playlist)
.FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet
@@ -438,11 +499,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
await updatePlaylistOrder(Room).ConfigureAwait(false);
}
+ private IEnumerable upcomingItems => serverSidePlaylist.Where(i => !i.Expired).OrderBy(i => i.PlaylistOrder);
+
private async Task updateCurrentItem(MultiplayerRoom room, bool notify = true)
{
// Pick the next non-expired playlist item by playlist order, or default to the most-recently-expired item.
- MultiplayerPlaylistItem nextItem = serverSidePlaylist.Where(i => !i.Expired).OrderBy(i => i.PlaylistOrder).FirstOrDefault()
- ?? serverSidePlaylist.OrderByDescending(i => i.PlayedAt).First();
+ MultiplayerPlaylistItem nextItem = upcomingItems.FirstOrDefault() ?? serverSidePlaylist.OrderByDescending(i => i.PlayedAt).First();
currentIndex = serverSidePlaylist.IndexOf(nextItem);
diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs
index cdd3e47930..22aac79056 100644
--- a/osu.Game/Tests/Visual/SkinnableTestScene.cs
+++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs
@@ -15,6 +15,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
+using osu.Game.Database;
using osu.Game.Graphics.Sprites;
using osu.Game.IO;
using osu.Game.Rulesets;
@@ -158,6 +159,7 @@ namespace osu.Game.Tests.Visual
public IResourceStore Files => null;
public new IResourceStore Resources => base.Resources;
public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore);
+ RealmContextFactory IStorageResourceProvider.RealmContextFactory => null;
#endregion
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index adb25f46fe..0e8486cabc 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -20,7 +20,7 @@
-
+
@@ -31,15 +31,15 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
+
+
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index db5d9af865..42d8962c14 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -60,8 +60,8 @@
-
-
+
+
@@ -83,7 +83,7 @@
-
+