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

Merge branch 'master' into fl-cumulative-strain

This commit is contained in:
MBmasher 2021-12-21 06:06:19 +11:00 committed by GitHub
commit c71655a75e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
345 changed files with 8000 additions and 3371 deletions

View File

@ -27,10 +27,10 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2021.725.0",
"version": "2021.1210.0",
"commands": [
"localisation"
]
}
}
}
}

View File

@ -77,10 +77,6 @@ jobs:
run: msbuild osu.Android/osu.Android.csproj /restore /p:Configuration=Debug
build-only-ios:
# While this workflow technically *can* run, it fails as iOS builds are blocked by multiple issues.
# See https://github.com/ppy/osu-framework/issues/4677 for the details.
# The job can be unblocked once those issues are resolved and game deployments can happen again.
if: false
name: Build only (iOS)
runs-on: macos-latest
timeout-minutes: 60

View File

@ -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.**

View File

@ -10,3 +10,6 @@ T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal
T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead.

View File

@ -11,7 +11,7 @@
A free-to-win rhythm game. Rhythm is just a *click* away!
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the codename "*lazer*". As in sharper than cutting-edge.
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge.
## Status
@ -48,7 +48,7 @@ You can see some examples of custom rulesets by visiting the [custom ruleset dir
Please make sure you have the following prerequisites:
- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) or higher installed.
- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) installed.
- When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.

View File

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

View File

@ -0,0 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Desktop.LegacyIpc
{
/// <summary>
/// A difficulty calculation request from the legacy client.
/// </summary>
/// <remarks>
/// Synchronise any changes with osu!stable.
/// </remarks>
public class LegacyIpcDifficultyCalculationRequest
{
public string BeatmapFile { get; set; }
public int RulesetId { get; set; }
public int Mods { get; set; }
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Desktop.LegacyIpc
{
/// <summary>
/// A difficulty calculation response returned to the legacy client.
/// </summary>
/// <remarks>
/// Synchronise any changes with osu!stable.
/// </remarks>
public class LegacyIpcDifficultyCalculationResponse
{
public double StarRating { get; set; }
}
}

View File

@ -0,0 +1,53 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Platform;
using Newtonsoft.Json.Linq;
namespace osu.Desktop.LegacyIpc
{
/// <summary>
/// An <see cref="IpcMessage"/> that can be used to communicate to and from legacy clients.
/// <para>
/// In order to deserialise types at either end, types must be serialised as their <see cref="System.Type.AssemblyQualifiedName"/>,
/// however this cannot be done since osu!stable and osu!lazer live in two different assemblies.
/// <br />
/// To get around this, this class exists which serialises a payload (<see cref="LegacyIpcMessage.Data"/>) as an <see cref="System.Object"/> type,
/// which can be deserialised at either end because it is part of the core library (mscorlib / System.Private.CorLib).
/// The payload contains the data to be sent over the IPC channel.
/// <br />
/// At either end, Json.NET deserialises the payload into a <see cref="JObject"/> which is manually converted back into the expected <see cref="LegacyIpcMessage.Data"/> type,
/// which then further contains another <see cref="JObject"/> representing the data sent over the IPC channel whose type can likewise be lazily matched through
/// <see cref="LegacyIpcMessage.Data.MessageType"/>.
/// </para>
/// </summary>
/// <remarks>
/// Synchronise any changes with osu-stable.
/// </remarks>
public class LegacyIpcMessage : IpcMessage
{
public LegacyIpcMessage()
{
// Types/assemblies are not inter-compatible, so always serialise/deserialise into objects.
base.Type = typeof(object).FullName;
}
public new string Type => base.Type; // Hide setter.
public new object Value
{
get => base.Value;
set => base.Value = new Data
{
MessageType = value.GetType().Name,
MessageData = value
};
}
public class Data
{
public string MessageType { get; set; }
public object MessageData { get; set; }
}
}
}

View File

@ -0,0 +1,121 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using Newtonsoft.Json.Linq;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
#nullable enable
namespace osu.Desktop.LegacyIpc
{
/// <summary>
/// Provides IPC to legacy osu! clients.
/// </summary>
public class LegacyTcpIpcProvider : TcpIpcProvider
{
private static readonly Logger logger = Logger.GetLogger("legacy-ipc");
public LegacyTcpIpcProvider()
: base(45357)
{
MessageReceived += msg =>
{
try
{
logger.Add("Processing legacy IPC message...");
logger.Add($" {msg.Value}", LogLevel.Debug);
// See explanation in LegacyIpcMessage for why this is done this way.
var legacyData = ((JObject)msg.Value).ToObject<LegacyIpcMessage.Data>();
object value = parseObject((JObject)legacyData!.MessageData, legacyData.MessageType);
return new LegacyIpcMessage
{
Value = onLegacyIpcMessageReceived(value)
};
}
catch (Exception ex)
{
logger.Add($"Processing IPC message failed: {msg.Value}", exception: ex);
return null;
}
};
}
private object parseObject(JObject value, string type)
{
switch (type)
{
case nameof(LegacyIpcDifficultyCalculationRequest):
return value.ToObject<LegacyIpcDifficultyCalculationRequest>()
?? throw new InvalidOperationException($"Failed to parse request {value}");
case nameof(LegacyIpcDifficultyCalculationResponse):
return value.ToObject<LegacyIpcDifficultyCalculationResponse>()
?? throw new InvalidOperationException($"Failed to parse request {value}");
default:
throw new ArgumentException($"Unsupported object type {type}");
}
}
private object onLegacyIpcMessageReceived(object message)
{
switch (message)
{
case LegacyIpcDifficultyCalculationRequest req:
try
{
var ruleset = getLegacyRulesetFromID(req.RulesetId);
Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray();
WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile, _ => ruleset);
return new LegacyIpcDifficultyCalculationResponse
{
StarRating = ruleset.CreateDifficultyCalculator(beatmap).Calculate(mods).StarRating
};
}
catch
{
return new LegacyIpcDifficultyCalculationResponse();
}
default:
throw new ArgumentException($"Unsupported message type {message}");
}
}
private static Ruleset getLegacyRulesetFromID(int rulesetId)
{
switch (rulesetId)
{
case 0:
return new OsuRuleset();
case 1:
return new TaikoRuleset();
case 2:
return new CatchRuleset();
case 3:
return new ManiaRuleset();
default:
throw new ArgumentException("Invalid ruleset id");
}
}
}
}

View File

@ -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);

View File

@ -5,6 +5,7 @@ using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using osu.Desktop.LegacyIpc;
using osu.Framework;
using osu.Framework.Development;
using osu.Framework.Logging;
@ -18,8 +19,10 @@ namespace osu.Desktop
{
private const string base_game_name = @"osu";
private static LegacyTcpIpcProvider legacyIpc;
[STAThread]
public static int Main(string[] args)
public static void Main(string[] args)
{
// Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory;
@ -69,14 +72,28 @@ namespace osu.Desktop
throw new TimeoutException(@"IPC took too long to send");
}
return 0;
return;
}
// we want to allow multiple instances to be started when in debug.
if (!DebugUtils.IsDebugBuild)
{
Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error);
return 0;
return;
}
}
if (host.IsPrimaryInstance)
{
try
{
Logger.Log("Starting legacy IPC provider...");
legacyIpc = new LegacyTcpIpcProvider();
legacyIpc.Bind();
}
catch (Exception ex)
{
Logger.Error(ex, "Failed to start legacy IPC provider");
}
}
@ -84,8 +101,6 @@ namespace osu.Desktop
host.Run(new TournamentGame());
else
host.Run(new OsuGameDesktop(args));
return 0;
}
}

View File

@ -32,5 +32,7 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

@ -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<ConvertValue> 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;
}
}

View File

@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private Drawable setupSkinHierarchy(Drawable child, ISkin skin)
{
var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info));
var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.CreateInfo()));
var testSkinProvider = new SkinProvidingContainer(skin);
var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider));

View File

@ -0,0 +1,19 @@
{
"Mappings": [{
"StartTime": 369,
"Objects": [{
"StartTime": 369,
"Position": 0,
"HyperDash": true
}]
},
{
"StartTime": 450,
"Objects": [{
"StartTime": 450,
"Position": 512,
"HyperDash": false
}]
}
]
}

View File

@ -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:

View File

@ -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
}
]
}]

View File

@ -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
}
]
}]

View File

@ -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
}]
}
]

View File

@ -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
}]
}
]

View File

@ -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}]}]}
{
"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
}
]
}]
}

View File

@ -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
}]
}
]

View File

@ -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
}
]
}]

View File

@ -32,5 +32,7 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Screens.Edit;
@ -55,13 +56,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestDefaultSkin()
{
AddStep("set default skin", () => skins.CurrentSkinInfo.Value = SkinInfo.Default);
AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLiveUnmanaged());
}
[Test]
public void TestLegacySkin()
{
AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info);
AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged());
}
}
}

View File

@ -9,7 +9,6 @@ using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.UI;
@ -46,12 +45,6 @@ namespace osu.Game.Rulesets.Mania.Edit
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
[Resolved]
private Bindable<WorkingBeatmap> working { get; set; }
[Resolved]
private OsuColour colours { get; set; }

View File

@ -7,16 +7,12 @@ using osu.Framework.Allocation;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Mania.Edit
{
public class ManiaSelectionHandler : EditorSelectionHandler
{
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
[Resolved]
private HitObjectComposer composer { get; set; }

View File

@ -15,9 +15,6 @@ namespace osu.Game.Rulesets.Mania.UI
public JudgementResult Result { get; private set; }
[Resolved]
private Column column { get; set; }
private SkinnableDrawable skinnableExplosion;
public PoolableHitExplosion()

View File

@ -32,5 +32,7 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("create slider", () =>
{
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.Info);
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
Child = new SkinProvidingContainer(tintingSkin)

View File

@ -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;
/// <summary>
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double JumpDistance { get; private set; }
public readonly double StrainTime;
/// <summary>
/// Minimum distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// Normalised distance from the "lazy" end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// <para>
/// 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).
/// </para>
/// </summary>
public double MovementDistance { get; private set; }
public double LazyJumpDistance { get; private set; }
/// <summary>
/// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>.
/// Normalised shortest distance to consider for a jump between the previous <see cref="OsuDifficultyHitObject"/> and this <see cref="OsuDifficultyHitObject"/>.
/// </summary>
/// <remarks>
/// This is bounded from above by <see cref="LazyJumpDistance"/>, and is smaller than the former if a more natural path is able to be taken through the previous <see cref="OsuDifficultyHitObject"/>.
/// </remarks>
/// <example>
/// Suppose a linear slider - circle pattern.
/// <br />
/// Following the slider lazily (see: <see cref="LazyJumpDistance"/>) will result in underestimating the true end position of the slider as being closer towards the start position.
/// As a result, <see cref="LazyJumpDistance"/> 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.
/// <br />
/// 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.
/// </example>
public double MinimumJumpDistance { get; private set; }
/// <summary>
/// The time taken to travel through <see cref="MinimumJumpDistance"/>, with a minimum value of 25ms.
/// </summary>
public double MinimumJumpTime { get; private set; }
/// <summary>
/// Normalised distance between the start and end position of this <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double TravelDistance { get; private set; }
/// <summary>
/// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for a non-zero distance.
/// </summary>
public double TravelTime { get; private set; }
/// <summary>
/// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>.
/// Calculated as the angle between the circles (current-2, current-1, current).
/// </summary>
public double? Angle { get; private set; }
/// <summary>
/// Milliseconds elapsed since the end time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double MovementTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/> to the end time of the same previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double TravelTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
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)

View File

@ -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.

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
// We also want to nerf stacks so that only the first object of the stack is accounted for.
double stackNerf = Math.Min(1.0, (osuLoop.JumpDistance / scalingFactor) / 25.0);
double stackNerf = Math.Min(1.0, (osuLoop.LazyJumpDistance / scalingFactor) / 25.0);
result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime;
}

View File

@ -55,73 +55,75 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
bool firstDeltaSwitch = false;
for (int i = Previous.Count - 2; i > 0; i--)
int rhythmStart = 0;
while (rhythmStart < Previous.Count - 2 && current.StartTime - Previous[rhythmStart].StartTime < history_time_max)
rhythmStart++;
for (int i = rhythmStart; i > 0; i--)
{
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1];
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i];
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)Previous[i + 1];
double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
if (currHistoricalDecay != 0)
currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
double currDelta = currObj.StrainTime;
double prevDelta = prevObj.StrainTime;
double lastDelta = lastObj.StrainTime;
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
windowPenalty = Math.Min(1, windowPenalty);
double effectiveRatio = windowPenalty * currRatio;
if (firstDeltaSwitch)
{
currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
double currDelta = currObj.StrainTime;
double prevDelta = prevObj.StrainTime;
double lastDelta = lastObj.StrainTime;
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
windowPenalty = Math.Min(1, windowPenalty);
double effectiveRatio = windowPenalty * currRatio;
if (firstDeltaSwitch)
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
{
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
{
if (islandSize < 7)
islandSize++; // island is still progressing, count size.
}
else
{
if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
effectiveRatio *= 0.125;
if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
effectiveRatio *= 0.25;
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
effectiveRatio *= 0.25;
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
effectiveRatio *= 0.50;
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
effectiveRatio *= 0.125;
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
startRatio = effectiveRatio;
previousIslandSize = islandSize; // log the last island size.
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
islandSize = 1;
}
if (islandSize < 7)
islandSize++; // island is still progressing, count size.
}
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
else
{
// Begin counting island until we change speed again.
firstDeltaSwitch = true;
if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
effectiveRatio *= 0.125;
if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
effectiveRatio *= 0.25;
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
effectiveRatio *= 0.25;
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
effectiveRatio *= 0.50;
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
effectiveRatio *= 0.125;
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
startRatio = effectiveRatio;
previousIslandSize = islandSize; // log the last island size.
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
islandSize = 1;
}
}
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
{
// Begin counting island until we change speed again.
firstDeltaSwitch = true;
startRatio = effectiveRatio;
islandSize = 1;
}
}
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
@ -154,7 +156,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
if (strainTime < min_speed_bonus)
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance);
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance);
return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
}

View File

@ -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;

View File

@ -32,5 +32,7 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

@ -12,7 +12,6 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK;
@ -149,9 +148,6 @@ namespace osu.Game.Rulesets.Taiko.UI
centreHit.Colour = colours.Pink;
}
[Resolved(canBeNull: true)]
private GameplayClock gameplayClock { get; set; }
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
{
Drawable target = null;

View File

@ -32,5 +32,7 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

@ -2,14 +2,20 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko;
using osu.Game.Scoring;
@ -21,6 +27,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
[TestFixture]
public class LegacyScoreDecoderTest
{
private CultureInfo originalCulture;
[SetUp]
public void SetUp()
{
originalCulture = CultureInfo.CurrentCulture;
}
[Test]
public void TestDecodeManiaReplay()
{
@ -44,6 +58,59 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
[Test]
public void TestCultureInvariance()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset);
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
}
}
};
// the "se" culture is used here, as it encodes the negative number sign as U+2212 MINUS SIGN,
// rather than the classic ASCII U+002D HYPHEN-MINUS.
CultureInfo.CurrentCulture = new CultureInfo("se");
var encodeStream = new MemoryStream();
var encoder = new LegacyScoreEncoder(score, beatmap);
encoder.Encode(encodeStream);
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
var decoder = new TestLegacyScoreDecoder();
var decodedAfterEncode = decoder.Parse(decodeStream);
Assert.Multiple(() =>
{
Assert.That(decodedAfterEncode, Is.Not.Null);
Assert.That(decodedAfterEncode.ScoreInfo.User.Username, Is.EqualTo(scoreInfo.User.Username));
Assert.That(decodedAfterEncode.ScoreInfo.BeatmapInfoID, Is.EqualTo(scoreInfo.BeatmapInfoID));
Assert.That(decodedAfterEncode.ScoreInfo.Ruleset, Is.EqualTo(scoreInfo.Ruleset));
Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(scoreInfo.TotalScore));
Assert.That(decodedAfterEncode.ScoreInfo.MaxCombo, Is.EqualTo(scoreInfo.MaxCombo));
Assert.That(decodedAfterEncode.ScoreInfo.Date, Is.EqualTo(scoreInfo.Date));
Assert.That(decodedAfterEncode.Replay.Frames.Count, Is.EqualTo(1));
});
}
[TearDown]
public void TearDown()
{
CultureInfo.CurrentCulture = originalCulture;
}
private class TestLegacyScoreDecoder : LegacyScoreDecoder
{
private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[]

View File

@ -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());

View File

@ -25,9 +25,6 @@ namespace osu.Game.Tests.Beatmaps
private BeatmapSetInfo importedSet;
[Resolved]
private BeatmapManager beatmaps { get; set; }
private TestBeatmapDifficultyCache difficultyCache;
private IBindable<StarDifficulty?> starDifficultyBindable;

View File

@ -809,7 +809,7 @@ namespace osu.Game.Tests.Database
// TODO: reimplement when we have score support in realm.
// return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
// {
// OnlineScoreID = 2,
// OnlineID = 2,
// Beatmap = beatmap,
// BeatmapInfoID = beatmap.ID
// }, new ImportScoreTest.TestArchiveReader());
@ -852,7 +852,11 @@ namespace osu.Game.Tests.Database
{
IQueryable<RealmBeatmapSet>? resultSets = null;
waitForOrAssert(() => (resultSets = realm.All<RealmBeatmapSet>().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any(),
waitForOrAssert(() =>
{
realm.Refresh();
return (resultSets = realm.All<RealmBeatmapSet>().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any();
},
@"BeatmapSet did not import to the database in allocated time.", timeout);
// ensure we were stored to beatmap database backing...
@ -865,16 +869,16 @@ namespace osu.Game.Tests.Database
// ReSharper disable once PossibleUnintendedReferenceComparison
IEnumerable<RealmBeatmap> queryBeatmaps() => realm.All<RealmBeatmap>().Where(s => s.BeatmapSet != null && s.BeatmapSet == set);
waitForOrAssert(() => queryBeatmaps().Count() == 12, @"Beatmaps did not import to the database in allocated time", timeout);
waitForOrAssert(() => queryBeatmapSets().Count() == 1, @"BeatmapSet did not import to the database in allocated time", timeout);
Assert.AreEqual(12, queryBeatmaps().Count(), @"Beatmap count was not correct");
Assert.AreEqual(1, queryBeatmapSets().Count(), @"Beatmapset count was not correct");
int countBeatmapSetBeatmaps = 0;
int countBeatmaps = 0;
int countBeatmapSetBeatmaps;
int countBeatmaps;
waitForOrAssert(() =>
(countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) ==
(countBeatmaps = queryBeatmaps().Count()),
$@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout);
Assert.AreEqual(
countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count,
countBeatmaps = queryBeatmaps().Count(),
$@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).");
foreach (RealmBeatmap b in set.Beatmaps)
Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID));

View File

@ -5,6 +5,8 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Game.Database;
using osu.Game.Models;
#nullable enable
@ -33,6 +35,39 @@ namespace osu.Game.Tests.Database
});
}
/// <summary>
/// Test to ensure that a `CreateContext` call nested inside a subscription doesn't cause any deadlocks
/// due to context fetching semaphores.
/// </summary>
[Test]
public void TestNestedContextCreationWithSubscription()
{
RunTestWithRealm((realmFactory, _) =>
{
bool callbackRan = false;
using (var context = realmFactory.CreateContext())
{
var subscription = context.All<RealmBeatmap>().QueryAsyncWithNotifications((sender, changes, error) =>
{
using (realmFactory.CreateContext())
{
callbackRan = true;
}
});
// Force the callback above to run.
using (realmFactory.CreateContext())
{
}
subscription?.Dispose();
}
Assert.IsTrue(callbackRan);
});
}
[Test]
public void TestBlockOperationsWithContention()
{

View File

@ -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<RealmBeatmap> beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive();
ILive<RealmBeatmap> beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(realmFactory);
ILive<RealmBeatmap> beatmap2 = realmFactory.CreateContext().All<RealmBeatmap>().First().ToLive();
ILive<RealmBeatmap> beatmap2 = realmFactory.CreateContext().All<RealmBeatmap>().First().ToLive(realmFactory);
Assert.AreEqual(beatmap, beatmap2);
});
}
[Test]
public void TestAccessAfterStorageMigrate()
{
RunTestWithRealm((realmFactory, storage) =>
{
var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
ILive<RealmBeatmap> liveBeatmap;
using (var context = realmFactory.CreateContext())
{
context.Write(r => r.Add(beatmap));
liveBeatmap = beatmap.ToLive(realmFactory);
}
using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target"))
{
migratedStorage.DeleteDirectory(string.Empty);
storage.Migrate(migratedStorage);
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
}
});
}
[Test]
public void TestAccessAfterAttach()
{
@ -36,7 +64,7 @@ namespace osu.Game.Tests.Database
{
var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
var liveBeatmap = beatmap.ToLive();
var liveBeatmap = beatmap.ToLive(realmFactory);
using (var context = realmFactory.CreateContext())
context.Write(r => r.Add(beatmap));
@ -49,7 +77,7 @@ namespace osu.Game.Tests.Database
public void TestAccessNonManaged()
{
var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
var liveBeatmap = beatmap.ToLive();
var liveBeatmap = beatmap.ToLiveUnmanaged();
Assert.IsFalse(beatmap.Hidden);
Assert.IsFalse(liveBeatmap.Value.Hidden);
@ -62,43 +90,6 @@ namespace osu.Game.Tests.Database
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
}
[Test]
public void TestValueAccessWithOpenContext()
{
RunTestWithRealm((realmFactory, _) =>
{
ILive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
Assert.DoesNotThrow(() =>
{
using (realmFactory.CreateContext())
{
var resolved = liveBeatmap.Value;
Assert.IsTrue(resolved.Realm.IsClosed);
Assert.IsTrue(resolved.IsValid);
// can access properties without a crash.
Assert.IsFalse(resolved.Hidden);
}
});
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test]
public void TestScopedReadWithoutContext()
{
@ -111,7 +102,7 @@ namespace osu.Game.Tests.Database
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
liveBeatmap = beatmap.ToLive(realmFactory);
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
@ -140,7 +131,7 @@ namespace osu.Game.Tests.Database
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
liveBeatmap = beatmap.ToLive(realmFactory);
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
@ -154,6 +145,60 @@ namespace osu.Game.Tests.Database
});
}
[Test]
public void TestValueAccessNonManaged()
{
RunTestWithRealm((realmFactory, _) =>
{
var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
var liveBeatmap = beatmap.ToLive(realmFactory);
Assert.DoesNotThrow(() =>
{
var __ = liveBeatmap.Value;
});
});
}
[Test]
public void TestValueAccessWithOpenContextFails()
{
RunTestWithRealm((realmFactory, _) =>
{
ILive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive(realmFactory);
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
// Can't be used, without a valid context.
Assert.Throws<InvalidOperationException>(() =>
{
var __ = liveBeatmap.Value;
});
// Can't be used, even from within a valid context.
using (realmFactory.CreateContext())
{
Assert.Throws<InvalidOperationException>(() =>
{
var __ = liveBeatmap.Value;
});
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test]
public void TestValueAccessWithoutOpenContextFails()
{
@ -166,7 +211,7 @@ namespace osu.Game.Tests.Database
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
liveBeatmap = beatmap.ToLive(realmFactory);
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
@ -191,7 +236,7 @@ namespace osu.Game.Tests.Database
using (var updateThreadContext = realmFactory.CreateContext())
{
updateThreadContext.All<RealmBeatmap>().SubscribeForNotifications(gotChange);
updateThreadContext.All<RealmBeatmap>().QueryAsyncWithNotifications(gotChange);
ILive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
@ -205,7 +250,7 @@ namespace osu.Game.Tests.Database
// not just a refresh from the resolved Live.
threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
liveBeatmap = beatmap.ToLive(realmFactory);
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
@ -215,23 +260,22 @@ namespace osu.Game.Tests.Database
Assert.AreEqual(0, updateThreadContext.All<RealmBeatmap>().Count());
Assert.AreEqual(0, changesTriggered);
var resolved = liveBeatmap.Value;
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
Assert.AreEqual(2, updateThreadContext.All<RealmBeatmap>().Count());
Assert.AreEqual(1, changesTriggered);
// even though the realm that this instance was resolved for was closed, it's still valid.
Assert.IsTrue(resolved.Realm.IsClosed);
Assert.IsTrue(resolved.IsValid);
// can access properties without a crash.
Assert.IsFalse(resolved.Hidden);
updateThreadContext.Write(r =>
liveBeatmap.PerformRead(resolved =>
{
// can use with the main context.
r.Remove(resolved);
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
// ReSharper disable once AccessToDisposedClosure
Assert.AreEqual(2, updateThreadContext.All<RealmBeatmap>().Count());
Assert.AreEqual(1, changesTriggered);
// can access properties without a crash.
Assert.IsFalse(resolved.Hidden);
// ReSharper disable once AccessToDisposedClosure
updateThreadContext.Write(r =>
{
// can use with the main context.
r.Remove(resolved);
});
});
}

View File

@ -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<RealmContextFactory, Storage> testAction, [CallerMemberName] string caller = "")
protected void RunTestWithRealm(Action<RealmContextFactory, OsuStorage> 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);

View File

@ -45,9 +45,9 @@ namespace osu.Game.Tests.Database
{
var rulesets = new RealmRulesetStore(realmFactory, storage);
Assert.IsTrue((rulesets.AvailableRulesets.First() as RealmRuleset)?.IsManaged == false);
Assert.IsTrue((rulesets.GetRuleset(0) as RealmRuleset)?.IsManaged == false);
Assert.IsTrue((rulesets.GetRuleset("mania") as RealmRuleset)?.IsManaged == false);
Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged);
Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged);
Assert.IsFalse(rulesets.GetRuleset("mania")?.IsManaged);
});
}
}

View File

@ -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<RulesetInfo>());
Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(1));
}
private int queryCount(GlobalAction? match = null)
{
using (var realm = realmContextFactory.CreateContext())

View File

@ -15,6 +15,7 @@ using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@ -167,7 +168,7 @@ namespace osu.Game.Tests.Gameplay
private class TestSkin : LegacySkin
{
public TestSkin(string resourceName, IStorageResourceProvider resources)
: base(DefaultLegacySkin.Info, new TestResourceStore(resourceName), resources, "skin.ini")
: base(DefaultLegacySkin.CreateInfo(), new TestResourceStore(resourceName), resources, "skin.ini")
{
}
}
@ -220,6 +221,7 @@ namespace osu.Game.Tests.Gameplay
public AudioManager AudioManager => Audio;
public IResourceStore<byte[]> Files => null;
public new IResourceStore<byte[]> Resources => base.Resources;
public RealmContextFactory RealmContextFactory => null;
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => null;
#endregion

View File

@ -18,9 +18,6 @@ namespace osu.Game.Tests.Input
[Resolved]
private FrameworkConfigManager frameworkConfigManager { get; set; }
[Resolved]
private OsuConfigManager osuConfigManager { get; set; }
[TestCase(WindowMode.Windowed)]
[TestCase(WindowMode.Borderless)]
public void TestDisableConfining(WindowMode windowMode)

View File

@ -3,12 +3,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
namespace osu.Game.Tests.NonVisual
{
@ -20,8 +22,10 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator().CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(1, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
assertCombinations(new[]
{
new[] { typeof(ModNoMod) }
}, combinations);
}
[Test]
@ -29,9 +33,11 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(2, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
assertCombinations(new[]
{
new[] { typeof(ModNoMod) },
new[] { typeof(ModA) }
}, combinations);
}
[Test]
@ -39,14 +45,13 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(4, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is MultiMod);
Assert.IsTrue(combinations[3] is ModB);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
assertCombinations(new[]
{
new[] { typeof(ModNoMod) },
new[] { typeof(ModA) },
new[] { typeof(ModA), typeof(ModB) },
new[] { typeof(ModB) }
}, combinations);
}
[Test]
@ -54,10 +59,12 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is ModIncompatibleWithA);
assertCombinations(new[]
{
new[] { typeof(ModNoMod) },
new[] { typeof(ModA) },
new[] { typeof(ModIncompatibleWithA) }
}, combinations);
}
[Test]
@ -65,22 +72,17 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(8, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is MultiMod);
Assert.IsTrue(combinations[3] is ModB);
Assert.IsTrue(combinations[4] is MultiMod);
Assert.IsTrue(combinations[5] is ModIncompatibleWithA);
Assert.IsTrue(combinations[6] is MultiMod);
Assert.IsTrue(combinations[7] is ModIncompatibleWithAAndB);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
Assert.IsTrue(((MultiMod)combinations[4]).Mods[0] is ModB);
Assert.IsTrue(((MultiMod)combinations[4]).Mods[1] is ModIncompatibleWithA);
Assert.IsTrue(((MultiMod)combinations[6]).Mods[0] is ModIncompatibleWithA);
Assert.IsTrue(((MultiMod)combinations[6]).Mods[1] is ModIncompatibleWithAAndB);
assertCombinations(new[]
{
new[] { typeof(ModNoMod) },
new[] { typeof(ModA) },
new[] { typeof(ModA), typeof(ModB) },
new[] { typeof(ModB) },
new[] { typeof(ModB), typeof(ModIncompatibleWithA) },
new[] { typeof(ModIncompatibleWithA) },
new[] { typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB) },
new[] { typeof(ModIncompatibleWithAAndB) },
}, combinations);
}
[Test]
@ -88,10 +90,12 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModAofA);
Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA);
assertCombinations(new[]
{
new[] { typeof(ModNoMod) },
new[] { typeof(ModAofA) },
new[] { typeof(ModIncompatibleWithAofA) }
}, combinations);
}
[Test]
@ -99,17 +103,13 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(4, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is MultiMod);
Assert.IsTrue(combinations[3] is MultiMod);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[2] is ModC);
Assert.IsTrue(((MultiMod)combinations[3]).Mods[0] is ModB);
Assert.IsTrue(((MultiMod)combinations[3]).Mods[1] is ModC);
assertCombinations(new[]
{
new[] { typeof(ModNoMod) },
new[] { typeof(ModA) },
new[] { typeof(ModA), typeof(ModB), typeof(ModC) },
new[] { typeof(ModB), typeof(ModC) }
}, combinations);
}
[Test]
@ -117,13 +117,12 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is MultiMod);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModB);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA);
assertCombinations(new[]
{
new[] { typeof(ModNoMod) },
new[] { typeof(ModA) },
new[] { typeof(ModB), typeof(ModIncompatibleWithA) }
}, combinations);
}
[Test]
@ -131,13 +130,28 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is MultiMod);
assertCombinations(new[]
{
new[] { typeof(ModNoMod) },
new[] { typeof(ModA) },
new[] { typeof(ModA), typeof(ModB) }
}, combinations);
}
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
private void assertCombinations(Type[][] expectedCombinations, Mod[] actualCombinations)
{
Assert.AreEqual(expectedCombinations.Length, actualCombinations.Length);
Assert.Multiple(() =>
{
for (int i = 0; i < expectedCombinations.Length; ++i)
{
Type[] expectedTypes = expectedCombinations[i];
Type[] actualTypes = ModUtils.FlattenMod(actualCombinations[i]).Select(m => m.GetType()).ToArray();
Assert.That(expectedTypes, Is.EquivalentTo(actualTypes));
}
});
}
private class ModA : Mod

View File

@ -16,6 +16,27 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
[HeadlessTest]
public class StatefulMultiplayerClientTest : MultiplayerTestScene
{
[Test]
public void TestUserAddedOnJoin()
{
var user = new APIUser { Id = 33 };
AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3);
AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
}
[Test]
public void TestUserRemovedOnLeave()
{
var user = new APIUser { Id = 44 };
AddStep("add user", () => Client.AddUser(user));
AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3);
AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1);
}
[Test]
public void TestPlayingUserTracking()
{
@ -24,8 +45,6 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddRepeatStep("add some users", () => Client.AddUser(new APIUser { Id = id++ }), 5);
checkPlayingUserCount(0);
AddAssert("playlist item is available", () => Client.CurrentMatchPlayingItem.Value != null);
changeState(3, MultiplayerUserState.WaitingForLoad);
checkPlayingUserCount(3);
@ -43,8 +62,6 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddStep("leave room", () => Client.LeaveRoom());
checkPlayingUserCount(0);
AddAssert("playlist item is null", () => Client.CurrentMatchPlayingItem.Value == null);
}
[Test]

View File

@ -0,0 +1,90 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.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"));
}
}
}

View File

@ -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<SubmittableScore>(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 } } }
});

View File

@ -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<BeatmapAvailability> expected)

View File

@ -1,35 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.OnlinePlay
{
[HeadlessTest]
public class StatefulMultiplayerClientTest : MultiplayerTestScene
{
[Test]
public void TestUserAddedOnJoin()
{
var user = new APIUser { Id = 33 };
AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3);
AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
}
[Test]
public void TestUserRemovedOnLeave()
{
var user = new APIUser { Id = 44 };
AddStep("add user", () => Client.AddUser(user));
AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3);
AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1);
}
}
}

View File

@ -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
}
}
}
/// <summary>
/// Create a test score model.
/// </summary>
/// <param name="ruleset">The ruleset for which the score was set against.</param>
/// <returns></returns>
public static ScoreInfo CreateTestScoreInfo(RulesetInfo ruleset = null) =>
CreateTestScoreInfo(CreateTestBeatmapSetInfo(1, new[] { ruleset ?? new OsuRuleset().RulesetInfo }).Beatmaps.First());
/// <summary>
/// Create a test score model.
/// </summary>
/// <param name="beatmap">The beatmap for which the score was set against.</param>
/// <returns></returns>
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, int>
{
[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;
}
}
}

View File

@ -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<ScoreManager>();
// 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
{

View File

@ -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()
{

View File

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Skinning;
@ -163,32 +164,109 @@ namespace osu.Game.Tests.Skins.IO
assertCorrectMetadata(import2, "name 1 [my custom skin 2]", "author 1", osu);
});
[Test]
public Task TestExportThenImportDefaultSkin() => runSkinTest(osu =>
{
var skinManager = osu.Dependencies.Get<SkinManager>();
skinManager.EnsureMutableSkin();
MemoryStream exportStream = new MemoryStream();
Guid originalSkinId = skinManager.CurrentSkinInfo.Value.ID;
skinManager.CurrentSkinInfo.Value.PerformRead(s =>
{
Assert.IsFalse(s.Protected);
Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType());
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
Assert.Greater(exportStream.Length, 0);
});
var imported = skinManager.Import(new ImportTask(exportStream, "exported.osk"));
imported.Result.PerformRead(s =>
{
Assert.IsFalse(s.Protected);
Assert.AreNotEqual(originalSkinId, s.ID);
Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType());
});
return Task.CompletedTask;
});
[Test]
public Task TestExportThenImportClassicSkin() => runSkinTest(osu =>
{
var skinManager = osu.Dependencies.Get<SkinManager>();
skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo;
skinManager.EnsureMutableSkin();
MemoryStream exportStream = new MemoryStream();
Guid originalSkinId = skinManager.CurrentSkinInfo.Value.ID;
skinManager.CurrentSkinInfo.Value.PerformRead(s =>
{
Assert.IsFalse(s.Protected);
Assert.AreEqual(typeof(DefaultLegacySkin), s.CreateInstance(skinManager).GetType());
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
Assert.Greater(exportStream.Length, 0);
});
var imported = skinManager.Import(new ImportTask(exportStream, "exported.osk"));
imported.Result.PerformRead(s =>
{
Assert.IsFalse(s.Protected);
Assert.AreNotEqual(originalSkinId, s.ID);
Assert.AreEqual(typeof(DefaultLegacySkin), s.CreateInstance(skinManager).GetType());
});
return Task.CompletedTask;
});
#endregion
private void assertCorrectMetadata(SkinInfo import1, string name, string creator, OsuGameBase osu)
private void assertCorrectMetadata(ILive<SkinInfo> import1, string name, string creator, OsuGameBase osu)
{
Assert.That(import1.Name, Is.EqualTo(name));
Assert.That(import1.Creator, Is.EqualTo(creator));
import1.PerformRead(i =>
{
Assert.That(i.Name, Is.EqualTo(name));
Assert.That(i.Creator, Is.EqualTo(creator));
// for extra safety let's reconstruct the skin, reading from the skin.ini.
var instance = import1.CreateInstance((IStorageResourceProvider)osu.Dependencies.Get(typeof(SkinManager)));
// for extra safety let's reconstruct the skin, reading from the skin.ini.
var instance = i.CreateInstance((IStorageResourceProvider)osu.Dependencies.Get(typeof(SkinManager)));
Assert.That(instance.Configuration.SkinInfo.Name, Is.EqualTo(name));
Assert.That(instance.Configuration.SkinInfo.Creator, Is.EqualTo(creator));
Assert.That(instance.Configuration.SkinInfo.Name, Is.EqualTo(name));
Assert.That(instance.Configuration.SkinInfo.Creator, Is.EqualTo(creator));
});
}
private void assertImportedBoth(SkinInfo import1, SkinInfo import2)
private void assertImportedBoth(ILive<SkinInfo> import1, ILive<SkinInfo> import2)
{
Assert.That(import2.ID, Is.Not.EqualTo(import1.ID));
Assert.That(import2.Hash, Is.Not.EqualTo(import1.Hash));
Assert.That(import2.Files.Select(f => f.FileInfoID), Is.Not.EquivalentTo(import1.Files.Select(f => f.FileInfoID)));
import1.PerformRead(i1 => import2.PerformRead(i2 =>
{
Assert.That(i2.ID, Is.Not.EqualTo(i1.ID));
Assert.That(i2.Hash, Is.Not.EqualTo(i1.Hash));
Assert.That(i2.Files.First(), Is.Not.EqualTo(i1.Files.First()));
}));
}
private void assertImportedOnce(SkinInfo import1, SkinInfo import2)
private void assertImportedOnce(ILive<SkinInfo> import1, ILive<SkinInfo> import2)
{
Assert.That(import2.ID, Is.EqualTo(import1.ID));
Assert.That(import2.Hash, Is.EqualTo(import1.Hash));
Assert.That(import2.Files.Select(f => f.FileInfoID), Is.EquivalentTo(import1.Files.Select(f => f.FileInfoID)));
import1.PerformRead(i1 => import2.PerformRead(i2 =>
{
Assert.That(i2.ID, Is.EqualTo(i1.ID));
Assert.That(i2.Hash, Is.EqualTo(i1.Hash));
Assert.That(i2.Files.First(), Is.EqualTo(i1.Files.First()));
}));
}
private MemoryStream createEmptyOsk()
@ -255,10 +333,10 @@ namespace osu.Game.Tests.Skins.IO
}
}
private async Task<SkinInfo> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null)
private async Task<ILive<SkinInfo>> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null)
{
var skinManager = osu.Dependencies.Get<SkinManager>();
return (await skinManager.Import(archive)).Value;
return await skinManager.Import(archive);
}
}
}

View File

@ -24,7 +24,7 @@ namespace osu.Game.Tests.Skins
private void load()
{
var imported = skins.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-skin.osk"))).Result;
skin = skins.GetSkin(imported.Value);
skin = imported.PerformRead(skinInfo => skins.GetSkin(skinInfo));
}
[Test]

View File

@ -5,15 +5,20 @@ using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics.Textures;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens;
using osu.Game.Screens.Backgrounds;
using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Background
{
@ -21,8 +26,7 @@ namespace osu.Game.Tests.Visual.Background
public class TestSceneBackgroundScreenDefault : OsuTestScene
{
private BackgroundScreenStack stack;
private BackgroundScreenDefault screen;
private TestBackgroundScreenDefault screen;
private Graphics.Backgrounds.Background getCurrentBackground() => screen.ChildrenOfType<Graphics.Backgrounds.Background>().FirstOrDefault();
[Resolved]
@ -35,10 +39,96 @@ namespace osu.Game.Tests.Visual.Background
public void SetUpSteps()
{
AddStep("create background stack", () => Child = stack = new BackgroundScreenStack());
AddStep("push default screen", () => stack.Push(screen = new BackgroundScreenDefault(false)));
AddStep("push default screen", () => stack.Push(screen = new TestBackgroundScreenDefault()));
AddUntilStep("wait for screen to load", () => screen.IsCurrentScreen());
}
[Test]
public void TestBeatmapBackgroundTracksBeatmap()
{
setSupporter(true);
setSourceMode(BackgroundSource.Beatmap);
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddAssert("background changed", () => screen.CheckLastLoadChange() == true);
Graphics.Backgrounds.Background last = null;
AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground));
AddStep("store background", () => last = getCurrentBackground());
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddUntilStep("wait for beatmap background to change", () => screen.CheckLastLoadChange() == true);
AddUntilStep("background is new beatmap background", () => last != getCurrentBackground());
AddStep("store background", () => last = getCurrentBackground());
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddUntilStep("wait for beatmap background to change", () => screen.CheckLastLoadChange() == true);
AddUntilStep("background is new beatmap background", () => last != getCurrentBackground());
}
[Test]
public void TestBeatmapBackgroundTracksBeatmapWhenSuspended()
{
setSupporter(true);
setSourceMode(BackgroundSource.Beatmap);
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddAssert("background changed", () => screen.CheckLastLoadChange() == true);
AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground));
BackgroundScreenBeatmap nestedScreen = null;
// of note, this needs to be a type that doesn't match BackgroundScreenDefault else it is silently not pushed by the background stack.
AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value)));
AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen());
AddUntilStep("previous background hidden", () => !screen.IsAlive);
AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null);
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null);
AddStep("pop screen back to top level", () => screen.MakeCurrent());
AddAssert("top level background changed", () => screen.CheckLastLoadChange() == true);
}
[Test]
public void TestBeatmapBackgroundIgnoresNoChangeWhenSuspended()
{
BackgroundScreenBeatmap nestedScreen = null;
WorkingBeatmap originalWorking = null;
setSupporter(true);
setSourceMode(BackgroundSource.Beatmap);
AddStep("change beatmap", () => originalWorking = Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddAssert("background changed", () => screen.CheckLastLoadChange() == true);
AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground));
// of note, this needs to be a type that doesn't match BackgroundScreenDefault else it is silently not pushed by the background stack.
AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value)));
AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen());
// we're testing a case where scheduling may be used to avoid issues, so ensure the scheduler is no longer running.
AddUntilStep("wait for top level not alive", () => !screen.IsAlive);
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddStep("change beatmap back", () => Beatmap.Value = originalWorking);
AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null);
AddStep("pop screen back to top level", () => screen.MakeCurrent());
AddStep("top level screen is current", () => screen.IsCurrentScreen());
AddAssert("top level background reused existing", () => screen.CheckLastLoadChange() == false);
}
[Test]
public void TestBackgroundTypeSwitch()
{
@ -77,36 +167,24 @@ namespace osu.Game.Tests.Visual.Background
[TestCase(BackgroundSource.Skin, typeof(SkinBackground))]
public void TestBackgroundDoesntReloadOnNoChange(BackgroundSource source, Type backgroundType)
{
Graphics.Backgrounds.Background last = null;
setSourceMode(source);
setSupporter(true);
if (source == BackgroundSource.Skin)
setCustomSkin();
AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground())?.GetType() == backgroundType);
AddUntilStep("wait for beatmap background to be loaded", () => (getCurrentBackground())?.GetType() == backgroundType);
AddAssert("next doesn't load new background", () => screen.Next() == false);
// doesn't really need to be checked but might as well.
AddWaitStep("wait a bit", 5);
AddUntilStep("ensure same background instance", () => last == getCurrentBackground());
}
[Test]
public void TestBackgroundCyclingOnDefaultSkin([Values] bool supporter)
{
Graphics.Backgrounds.Background last = null;
setSourceMode(BackgroundSource.Skin);
setSupporter(supporter);
setDefaultSkin();
AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground())?.GetType() == typeof(Graphics.Backgrounds.Background));
AddUntilStep("wait for beatmap background to be loaded", () => (getCurrentBackground())?.GetType() == typeof(Graphics.Backgrounds.Background));
AddAssert("next cycles background", () => screen.Next());
// doesn't really need to be checked but might as well.
AddWaitStep("wait a bit", 5);
AddUntilStep("ensure different background instance", () => last != getCurrentBackground());
}
private void setSourceMode(BackgroundSource source) =>
@ -119,10 +197,46 @@ namespace osu.Game.Tests.Visual.Background
Id = API.LocalUser.Value.Id + 1,
});
private WorkingBeatmap createTestWorkingBeatmapWithUniqueBackground() => new UniqueBackgroundTestWorkingBeatmap(Audio);
private class TestBackgroundScreenDefault : BackgroundScreenDefault
{
private bool? lastLoadTriggerCausedChange;
public TestBackgroundScreenDefault()
: base(false)
{
}
public override bool Next()
{
bool didChange = base.Next();
lastLoadTriggerCausedChange = didChange;
return didChange;
}
public bool? CheckLastLoadChange()
{
bool? lastChange = lastLoadTriggerCausedChange;
lastLoadTriggerCausedChange = null;
return lastChange;
}
}
private class UniqueBackgroundTestWorkingBeatmap : TestWorkingBeatmap
{
public UniqueBackgroundTestWorkingBeatmap(AudioManager audioManager)
: base(new Beatmap(), null, audioManager)
{
}
protected override Texture GetBackground() => new Texture(1, 1);
}
private void setCustomSkin()
{
// feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin.
AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo { ID = 5 });
AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo().ToLiveUnmanaged());
}
private void setDefaultSkin() => AddStep("set default skin", () => skins.CurrentSkinInfo.SetDefault());

View File

@ -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());

View File

@ -11,17 +11,18 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osuTK;
using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Tests.Visual.Beatmaps
{
public class TestSceneBeatmapCard : OsuTestScene
public class TestSceneBeatmapCard : OsuManualInputManagerTestScene
{
/// <summary>
/// All cards on this scene use a common online ID to ensure that map download, preview tracks, etc. can be tested manually with online sources.
@ -227,7 +228,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
new BasicScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
Child = new ReverseChildIDFillFlowContainer<Drawable>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@ -248,6 +249,35 @@ namespace osu.Game.Tests.Visual.Beatmaps
}
[Test]
public void TestNormal() => createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo));
public void TestNormal()
{
createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo));
}
[Test]
public void TestHoverState()
{
AddStep("create cards", () => Child = createContent(OverlayColourScheme.Blue, s => new BeatmapCard(s)));
AddStep("Hover card", () => InputManager.MoveMouseTo(firstCard()));
AddWaitStep("wait for potential state change", 5);
AddAssert("card is not expanded", () => !firstCard().Expanded.Value);
AddStep("Hover spectrum display", () => InputManager.MoveMouseTo(firstCard().ChildrenOfType<DifficultySpectrumDisplay>().Single()));
AddUntilStep("card is expanded", () => firstCard().Expanded.Value);
AddStep("Hover difficulty content", () => InputManager.MoveMouseTo(firstCard().ChildrenOfType<BeatmapCardDifficultyList>().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<BeatmapCard>().Last()));
AddUntilStep("card is not expanded", () => !firstCard().Expanded.Value);
BeatmapCard firstCard() => this.ChildrenOfType<BeatmapCard>().First();
}
}
}

View File

@ -0,0 +1,71 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.Beatmaps
{
public class TestSceneBeatmapCardDifficultyList : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
var beatmapSet = new APIBeatmapSet
{
Beatmaps = new[]
{
new APIBeatmap { RulesetID = 1, StarRating = 5.76, DifficultyName = "Oni" },
new APIBeatmap { RulesetID = 1, StarRating = 3.20, DifficultyName = "Muzukashii" },
new APIBeatmap { RulesetID = 1, StarRating = 2.45, DifficultyName = "Futsuu" },
new APIBeatmap { RulesetID = 0, StarRating = 2.04, DifficultyName = "Normal" },
new APIBeatmap { RulesetID = 0, StarRating = 3.51, DifficultyName = "Hard" },
new APIBeatmap { RulesetID = 0, StarRating = 5.25, DifficultyName = "Insane" },
new APIBeatmap { RulesetID = 2, StarRating = 2.64, DifficultyName = "Salad" },
new APIBeatmap { RulesetID = 2, StarRating = 3.56, DifficultyName = "Platter" },
new APIBeatmap { RulesetID = 2, StarRating = 4.65, DifficultyName = "Rain" },
new APIBeatmap { RulesetID = 3, StarRating = 1.93, DifficultyName = "[7K] Normal" },
new APIBeatmap { RulesetID = 3, StarRating = 3.18, DifficultyName = "[7K] Hyper" },
new APIBeatmap { RulesetID = 3, StarRating = 4.82, DifficultyName = "[7K] Another" },
new APIBeatmap { RulesetID = 4, StarRating = 9.99, DifficultyName = "Unknown?!" },
}
};
Child = new Container
{
Width = 300,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background2
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(10),
Child = new BeatmapCardDifficultyList(beatmapSet)
}
}
};
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Game.Beatmaps;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Beatmaps.IO;
@ -89,6 +90,7 @@ namespace osu.Game.Tests.Visual.Editing
confirmEditingBeatmap(() => targetDifficulty);
AddAssert("no objects selected", () => !EditorBeatmap.SelectedHitObjects.Any());
AddUntilStep("wait for drawable ruleset", () => Editor.ChildrenOfType<DrawableRuleset>().SingleOrDefault()?.IsLoaded == true);
AddStep("paste object", () => Editor.Paste());
if (sameRuleset)

View File

@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Editing
public void TestCreateNewBeatmap()
{
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0);
AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.IsManaged);
AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == false);
}

View File

@ -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<BackgroundScreenBeatmap>().Single();
return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0;
});
AddAssert("no mods selected", () => SelectedMods.Value.Count == 0);
}
[Test]

View File

@ -12,6 +12,7 @@ using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
@ -41,7 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestEmptyLegacyBeatmapSkinFallsBack()
{
CreateSkinTest(SkinInfo.Default, () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null));
CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null));
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
}
@ -52,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("setup skins", () =>
{
skinManager.CurrentSkinInfo.Value = gameCurrentSkin;
skinManager.CurrentSkinInfo.Value = gameCurrentSkin.ToLiveUnmanaged();
currentBeatmapSkin = getBeatmapSkin();
});
});

View File

@ -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;

View File

@ -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);

View File

@ -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);
}
/// <remarks>

View File

@ -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,

View File

@ -43,83 +43,88 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>());
[SetUp]
public void SetUp() => Schedule(() =>
[SetUpSteps]
public void SetUpSteps()
{
replay = new Replay();
AddStep("Reset recorder state", cleanUpState);
Add(new GridContainer
AddStep("Setup containers", () =>
{
RelativeSizeAxes = Axes.Both,
Content = new[]
replay = new Replay();
Add(new GridContainer
{
new Drawable[]
RelativeSizeAxes = Axes.Both,
Content = new[]
{
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
new Drawable[]
{
Recorder = recorder = new TestReplayRecorder(new Score
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
Replay = replay,
ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo }
})
{
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
Recorder = recorder = new TestReplayRecorder(new Score
{
new Box
Replay = replay,
ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo }
})
{
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Colour = Color4.Brown,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Recording",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
}
},
new Drawable[]
{
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
new Box
{
Colour = Color4.Brown,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Recording",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
}
},
new Drawable[]
{
ReplayInputHandler = new TestFramedReplayInputHandler(replay)
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
ReplayInputHandler = new TestFramedReplayInputHandler(replay)
{
new Box
GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Colour = Color4.DarkBlue,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Playback",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
new Box
{
Colour = Color4.DarkBlue,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Playback",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
}
}
}
}
});
});
});
}
[Test]
public void TestBasic()
@ -184,7 +189,14 @@ namespace osu.Game.Tests.Visual.Gameplay
[TearDownSteps]
public void TearDown()
{
AddStep("stop recorder", () => recorder.Expire());
AddStep("stop recorder", cleanUpState);
}
private void cleanUpState()
{
// Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`.
recorder?.RemoveAndDisposeImmediately();
recorder = null;
}
public class TestFramedReplayInputHandler : FramedReplayInputHandler<TestReplayFrame>

View File

@ -1,228 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneReplayRecording : OsuTestScene
{
private readonly TestRulesetInputManager playbackManager;
private readonly TestRulesetInputManager recordingManager;
[Cached]
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>());
public TestSceneReplayRecording()
{
Replay replay = new Replay();
Add(new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
Recorder = new TestReplayRecorder(new Score
{
Replay = replay,
ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo }
})
{
ScreenSpaceToGamefield = pos => recordingManager?.ToLocalSpace(pos) ?? Vector2.Zero,
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Brown,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Recording",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestConsumer()
}
},
}
},
new Drawable[]
{
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
ReplayInputHandler = new TestFramedReplayInputHandler(replay)
{
GamefieldToScreenSpace = pos => playbackManager?.ToScreenSpace(pos) ?? Vector2.Zero,
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.DarkBlue,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Playback",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestConsumer()
}
},
}
}
}
});
}
protected override void Update()
{
base.Update();
playbackManager.ReplayInputHandler.SetFrameFromTime(Time.Current - 500);
}
}
public class TestFramedReplayInputHandler : FramedReplayInputHandler<TestReplayFrame>
{
public TestFramedReplayInputHandler(Replay replay)
: base(replay)
{
}
public override void CollectPendingInputs(List<IInput> inputs)
{
inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
inputs.Add(new ReplayState<TestAction> { PressedActions = CurrentFrame?.Actions ?? new List<TestAction>() });
}
}
public class TestConsumer : CompositeDrawable, IKeyBindingHandler<TestAction>
{
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent.ReceivePositionalInputAt(screenSpacePos);
private readonly Box box;
public TestConsumer()
{
Size = new Vector2(30);
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
{
box = new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
};
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
Position = e.MousePosition;
return base.OnMouseMove(e);
}
public bool OnPressed(KeyBindingPressEvent<TestAction> e)
{
if (e.Repeat)
return false;
box.Colour = Color4.White;
return true;
}
public void OnReleased(KeyBindingReleaseEvent<TestAction> e)
{
box.Colour = Color4.Black;
}
}
public class TestRulesetInputManager : RulesetInputManager<TestAction>
{
public TestRulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
: base(ruleset, variant, unique)
{
}
protected override KeyBindingContainer<TestAction> CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
=> new TestKeyBindingContainer();
internal class TestKeyBindingContainer : KeyBindingContainer<TestAction>
{
public override IEnumerable<IKeyBinding> DefaultKeyBindings => new[]
{
new KeyBinding(InputKey.MouseLeft, TestAction.Down),
};
}
}
public class TestReplayFrame : ReplayFrame
{
public Vector2 Position;
public List<TestAction> Actions = new List<TestAction>();
public TestReplayFrame(double time, Vector2 position, params TestAction[] actions)
: base(time)
{
Position = position;
Actions.AddRange(actions);
}
}
public enum TestAction
{
Down,
}
internal class TestReplayRecorder : ReplayRecorder<TestAction>
{
public TestReplayRecorder(Score target)
: base(target)
{
}
protected override ReplayFrame HandleFrame(Vector2 mousePosition, List<TestAction> actions, ReplayFrame previousFrame) =>
new TestReplayFrame(Time.Current, mousePosition, actions.ToArray());
}
}

View File

@ -2,12 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
using osu.Game.Skinning.Editor;
namespace osu.Game.Tests.Visual.Gameplay
@ -16,9 +14,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
private SkinEditor skinEditor;
[Resolved]
private SkinManager skinManager { get; set; }
protected override bool Autoplay => true;
[SetUpSteps]

View File

@ -10,7 +10,6 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
@ -36,9 +35,6 @@ namespace osu.Game.Tests.Visual.Gameplay
private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();
[Resolved]
private OsuConfigManager config { get; set; }
[Test]
public void TestComboCounterIncrementing()
{

View File

@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestReplayRecorder recorder;
private readonly ManualClock manualClock = new ManualClock();
private ManualClock manualClock;
private OsuSpriteText latencyDisplay;
@ -66,113 +66,121 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>());
[SetUp]
public void SetUp() => Schedule(() =>
[SetUpSteps]
public void SetUpSteps()
{
replay = new Replay();
AddStep("Reset recorder state", cleanUpState);
users.BindTo(spectatorClient.PlayingUsers);
users.BindCollectionChanged((obj, args) =>
AddStep("Setup containers", () =>
{
switch (args.Action)
replay = new Replay();
manualClock = new ManualClock();
spectatorClient.OnNewFrames += onNewFrames;
users.BindTo(spectatorClient.PlayingUsers);
users.BindCollectionChanged((obj, args) =>
{
case NotifyCollectionChangedAction.Add:
Debug.Assert(args.NewItems != null);
foreach (int user in args.NewItems)
{
if (user == api.LocalUser.Value.Id)
spectatorClient.WatchUser(user);
}
break;
case NotifyCollectionChangedAction.Remove:
Debug.Assert(args.OldItems != null);
foreach (int user in args.OldItems)
{
if (user == api.LocalUser.Value.Id)
spectatorClient.StopWatchingUser(user);
}
break;
}
}, true);
spectatorClient.OnNewFrames += onNewFrames;
Add(new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
switch (args.Action)
{
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
case NotifyCollectionChangedAction.Add:
Debug.Assert(args.NewItems != null);
foreach (int user in args.NewItems)
{
if (user == api.LocalUser.Value.Id)
spectatorClient.WatchUser(user);
}
break;
case NotifyCollectionChangedAction.Remove:
Debug.Assert(args.OldItems != null);
foreach (int user in args.OldItems)
{
if (user == api.LocalUser.Value.Id)
spectatorClient.StopWatchingUser(user);
}
break;
}
}, true);
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
Recorder = recorder = new TestReplayRecorder
new Drawable[]
{
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
new Box
Recorder = recorder = new TestReplayRecorder
{
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
},
Child = new Container
{
Colour = Color4.Brown,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Brown,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Sending",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
new OsuSpriteText
{
Text = "Sending",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
new Drawable[]
{
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
Clock = new FramedClock(manualClock),
ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay)
{
GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.DarkBlue,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Receiving",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
}
}
}
},
new Drawable[]
{
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
Clock = new FramedClock(manualClock),
ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay)
{
GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.DarkBlue,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Receiving",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
}
}
}
latencyDisplay = new OsuSpriteText()
};
});
Add(latencyDisplay = new OsuSpriteText());
});
}
private void onNewFrames(int userId, FrameDataBundle frames)
{
@ -189,6 +197,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestBasic()
{
AddStep("Wait for user input", () => { });
}
private double latency = SpectatorClient.TIME_BETWEEN_SENDS;
@ -232,11 +241,15 @@ namespace osu.Game.Tests.Visual.Gameplay
[TearDownSteps]
public void TearDown()
{
AddStep("stop recorder", () =>
{
recorder.Expire();
spectatorClient.OnNewFrames -= onNewFrames;
});
AddStep("stop recorder", cleanUpState);
}
private void cleanUpState()
{
// Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`.
recorder?.RemoveAndDisposeImmediately();
recorder = null;
spectatorClient.OnNewFrames -= onNewFrames;
}
public class TestFramedReplayInputHandler : FramedReplayInputHandler<TestReplayFrame>

View File

@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Menus
private TestToolbar toolbar;
[Resolved]
private RulesetStore rulesets { get; set; }
private IRulesetStore rulesets { get; set; }
[SetUp]
public void SetUp() => Schedule(() =>

View File

@ -31,17 +31,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected BeatmapInfo InitialBeatmap { get; private set; }
protected BeatmapInfo OtherBeatmap { get; private set; }
protected IScreen CurrentScreen => multiplayerScreenStack.CurrentScreen;
protected IScreen CurrentSubScreen => multiplayerScreenStack.MultiplayerScreen.CurrentSubScreen;
protected IScreen CurrentScreen => multiplayerComponents.CurrentScreen;
protected IScreen CurrentSubScreen => multiplayerComponents.MultiplayerScreen.CurrentSubScreen;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
private TestMultiplayerScreenStack multiplayerScreenStack;
private TestMultiplayerComponents multiplayerComponents;
protected TestMultiplayerClient Client => multiplayerScreenStack.Client;
protected TestMultiplayerRoomManager RoomManager => multiplayerScreenStack.RoomManager;
protected TestMultiplayerClient Client => multiplayerComponents.Client;
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
@ -65,12 +64,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
OtherBeatmap = importedSet.Beatmaps.Last(b => b.RulesetID == 0);
});
AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack()));
AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded);
AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents()));
AddUntilStep("wait for multiplayer to load", () => multiplayerComponents.IsLoaded);
AddUntilStep("wait for lounge to load", () => this.ChildrenOfType<MultiplayerLoungeSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for lounge", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
AddStep("open room", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().Single().Open(new Room
AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open(new Room
{
Name = { Value = "Test Room" },
QueueMode = { Value = Mode },
@ -93,7 +92,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for join", () => RoomManager.RoomJoined);
AddUntilStep("wait for join", () => Client.RoomJoined);
}
[Test]
@ -110,8 +109,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready);
clickReadyButton();
AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player player && player.IsLoaded);
AddStep("exit player", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent());
AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded);
AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());
}
private void clickReadyButton()

View File

@ -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<BeatmapInfo> beatmap)
{
AddStep("click edit button", () =>
AddStep("click add button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().AddOrEditPlaylistButton);
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen.AddItemButton>().Single());
InputManager.Click(MouseButton.Left);
});

View File

@ -11,12 +11,14 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Tests.Beatmaps;
using osuTK;
@ -172,6 +174,39 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
}
[Test]
public void TestMultiplayerRooms()
{
AddStep("create rooms", () => Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Children = new[]
{
new DrawableMatchRoom(new Room
{
Name = { Value = "A host-only room" },
QueueMode = { Value = QueueMode.HostOnly },
Type = { Value = MatchType.HeadToHead }
}),
new DrawableMatchRoom(new Room
{
Name = { Value = "An all-players, team-versus room" },
QueueMode = { Value = QueueMode.AllPlayers },
Type = { Value = MatchType.TeamVersus }
}),
new DrawableMatchRoom(new Room
{
Name = { Value = "A round-robin room" },
QueueMode = { Value = QueueMode.AllPlayersRoundRobin },
Type = { Value = MatchType.HeadToHead }
}),
}
});
}
private DrawableRoom createLoungeRoom(Room room)
{
room.Host.Value ??= new APIUser { Username = "peppy", Id = 2 };

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.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<ScrollContainer<Drawable>>().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<BeatmapDownloadButton>().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<UpdateableAvatar>().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<DifficultyIcon>().ElementAt(index), offset));
@ -326,12 +268,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.MoveMouseTo(item.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>.PlaylistItemHandle>().Single(), offset);
});
private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () =>
{
var item = playlist.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>>().ElementAt(index);
InputManager.MoveMouseTo(item.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(0), offset);
});
private void assertHandleVisibility(int index, bool visible)
=> AddAssert($"handle {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>.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<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(2 + index * 2).Alpha > 0) == visible);
private void createPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false)
private void createPlaylist(Action<TestPlaylist> 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<PlaylistItem, RearrangeableListItem<PlaylistItem>> ItemMap => base.ItemMap;
public TestPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false)
: base(allowEdit, allowSelection, showItemOwner: showItemOwner)
{
}
}
}
}

View File

@ -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<BeatmapInfo> beatmap)
{
AddUntilStep("wait for playlist panels to load", () =>
{
var queueList = this.ChildrenOfType<MultiplayerQueueList>().Single();
return queueList.ChildrenOfType<DrawableRoomPlaylistItem>().Count() == queueList.Items.Count;
});
AddStep("click edit button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().AddOrEditPlaylistButton);
InputManager.MoveMouseTo(this.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistEditButton>().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<BeatmapInfo> beatmap)
{
AddStep("click add button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen.AddItemButton>().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);
}
}
}

View File

@ -2,11 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Components;
@ -18,12 +15,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene
{
[Resolved]
private BeatmapManager beatmapManager { get; set; }
[Resolved]
private RulesetStore rulesetStore { get; set; }
[SetUp]
public new void Setup() => Schedule(() =>
{

View File

@ -30,6 +30,8 @@ using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Spectate;
@ -44,10 +46,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
private TestMultiplayerScreenStack multiplayerScreenStack;
private TestMultiplayerComponents multiplayerComponents;
private TestMultiplayerClient client => multiplayerScreenStack.Client;
private TestMultiplayerRoomManager roomManager => multiplayerScreenStack.RoomManager;
private TestMultiplayerClient client => multiplayerComponents.Client;
private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager;
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
@ -69,8 +71,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
});
AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack()));
AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded);
AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents()));
AddUntilStep("wait for multiplayer to load", () => multiplayerComponents.IsLoaded);
AddUntilStep("wait for lounge to load", () => this.ChildrenOfType<MultiplayerLoungeSubScreen>().FirstOrDefault()?.IsLoaded == true);
}
@ -214,7 +216,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Press select", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => roomManager.RoomJoined);
AddUntilStep("wait for join", () => client.RoomJoined);
}
[Test]
@ -293,7 +295,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("join room", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => roomManager.RoomJoined);
AddUntilStep("wait for join", () => client.RoomJoined);
AddAssert("Check participant count correct", () => client.APIRoom?.ParticipantCount.Value == 1);
AddAssert("Check participant list contains user", () => client.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1);
@ -351,7 +353,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => roomManager.RoomJoined);
AddUntilStep("wait for join", () => client.RoomJoined);
}
[Test]
@ -391,12 +393,51 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
AddStep("set user ready", () => client.ChangeState(MultiplayerUserState.Ready));
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
pressReadyButton();
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle);
}
[Test]
public void TestPlayStartsWithCorrectBeatmapWhileAtSongSelect()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
pressReadyButton();
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID);
AddStep("Select next beatmap", () => InputManager.Key(Key.Down));
AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != client.Room?.Playlist.First().BeatmapID);
AddStep("start match externally", () => client.StartMatch());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID);
}
[Test]
public void TestLocalPlayDoesNotStartWhileSpectatingWithNoBeatmap()
{
@ -432,7 +473,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("start match externally", () => client.StartMatch());
AddAssert("play not started", () => multiplayerScreenStack.IsCurrentScreen());
AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen());
}
[Test]
@ -476,7 +517,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
});
AddUntilStep("play started", () => multiplayerScreenStack.CurrentScreen is SpectatorScreen);
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is SpectatorScreen);
}
[Test]
@ -518,16 +559,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("open mod overlay", () => this.ChildrenOfType<RoomSubScreen.UserModSelectButton>().Single().TriggerClick());
AddStep("invoke on back button", () => multiplayerScreenStack.OnBackButton());
AddStep("invoke on back button", () => multiplayerComponents.OnBackButton());
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<UserModSelectOverlay>().Single().State.Value == Visibility.Hidden);
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
testLeave("back button", () => multiplayerScreenStack.OnBackButton());
testLeave("back button", () => multiplayerComponents.OnBackButton());
// mimics home button and OS window close
testLeave("forced exit", () => multiplayerScreenStack.Exit());
testLeave("forced exit", () => multiplayerComponents.Exit());
void testLeave(string actionName, Action action)
{
@ -555,20 +596,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType<OsuButton>().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<OsuButton>().Single().Enabled.Value);
AddStep("click start button", () => InputManager.Click(MouseButton.Left));
AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player);
enterGameplay();
// Gameplay runs in real-time, so we need to incrementally check if gameplay has finished in order to not time out.
for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000)
@ -577,7 +605,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.GameplayClock.CurrentTime > time);
}
AddUntilStep("wait for results", () => multiplayerScreenStack.CurrentScreen is ResultsScreen);
AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen);
}
[Test]
@ -618,7 +646,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("join room", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => roomManager.RoomJoined);
AddUntilStep("wait for join", () => client.RoomJoined);
AddAssert("local room has correct settings", () =>
{
@ -628,12 +656,178 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
private MultiplayerReadyButton readyButton => this.ChildrenOfType<MultiplayerReadyButton>().Single();
[Test]
public void TestSpectatingStateResetOnBackButtonDuringGameplay()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
QueueMode = { Value = QueueMode.AllPlayers },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddStep("set spectating state", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("state set to spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready));
pressReadyButton(1234);
AddUntilStep("wait for gameplay", () => (multiplayerComponents.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true);
AddStep("press back button and exit", () =>
{
multiplayerComponents.OnBackButton();
multiplayerComponents.Exit();
});
AddUntilStep("wait for return to match subscreen", () => multiplayerComponents.MultiplayerScreen.IsCurrentScreen());
AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle);
}
[Test]
public void TestSpectatingStateNotResetOnBackButtonOutsideOfGameplay()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
QueueMode = { Value = QueueMode.AllPlayers },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddStep("set spectating state", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("state set to spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready));
pressReadyButton(1234);
AddUntilStep("wait for gameplay", () => (multiplayerComponents.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true);
AddStep("set other user loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded));
AddStep("set other user finished play", () => client.ChangeUserState(1234, MultiplayerUserState.FinishedPlay));
AddStep("press back button and exit", () =>
{
multiplayerComponents.OnBackButton();
multiplayerComponents.Exit();
});
AddUntilStep("wait for return to match subscreen", () => multiplayerComponents.MultiplayerScreen.IsCurrentScreen());
AddWaitStep("wait for possible state change", 5);
AddUntilStep("user state is spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
}
[Test]
public void TestItemAddedByOtherUserDuringGameplay()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
QueueMode = { Value = QueueMode.AllPlayers },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
enterGameplay();
AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem
{
BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1
})));
AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2);
AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());
AddUntilStep("queue contains item", () => this.ChildrenOfType<MultiplayerQueueList>().Single().Items.Single().ID == 2);
}
[Test]
public void TestItemAddedAndDeletedByOtherUserDuringGameplay()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
QueueMode = { Value = QueueMode.AllPlayers },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
enterGameplay();
AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem
{
BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1
})));
AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2);
AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2));
AddUntilStep("item removed from playlist", () => client.Room?.Playlist.Count == 1);
AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());
AddUntilStep("queue is empty", () => this.ChildrenOfType<MultiplayerQueueList>().Single().Items.Count == 0);
}
private void enterGameplay()
{
pressReadyButton();
pressReadyButton();
AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player);
}
private ReadyButton readyButton => this.ChildrenOfType<ReadyButton>().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> room)
{
AddUntilStep("wait for lounge", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
AddStep("open room", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().Single().Open(room()));
AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open(room()));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2);
@ -644,7 +838,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for join", () => roomManager.RoomJoined);
AddUntilStep("wait for join", () => client.RoomJoined);
}
}
}

View File

@ -144,7 +144,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set mods", () => SelectedMods.Value = new[] { new TaikoModDoubleTime() });
AddStep("confirm selection", () => songSelect.FinaliseSelection());
AddStep("exit song select", () => songSelect.Exit());
AddUntilStep("song select exited", () => !songSelect.IsCurrentScreen());
AddAssert("beatmap not changed", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap));
AddAssert("ruleset not changed", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo));
@ -178,7 +179,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public new BeatmapCarousel Carousel => base.Carousel;
public TestMultiplayerMatchSongSelect(Room room, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
: base(room, beatmap, ruleset)
: base(room, null, beatmap, ruleset)
{
}
}

View File

@ -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);

View File

@ -0,0 +1,234 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerPlaylist : MultiplayerTestScene
{
private MultiplayerPlaylist list;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
private BeatmapInfo importedBeatmap;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
}
[SetUp]
public new void Setup() => Schedule(() =>
{
Child = list = new MultiplayerPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.4f, 0.8f)
};
});
[SetUpSteps]
public new void SetUpSteps()
{
AddStep("import beatmap", () =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0);
});
AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }));
}
[Test]
public void TestNonExpiredItemsAddedToQueueList()
{
assertItemInQueueListStep(1, 0);
addItemStep();
assertItemInQueueListStep(2, 1);
addItemStep();
assertItemInQueueListStep(3, 2);
}
[Test]
public void TestExpiredItemsAddedToHistoryList()
{
assertItemInQueueListStep(1, 0);
addItemStep(true);
assertItemInHistoryListStep(2, 0);
addItemStep(true);
assertItemInHistoryListStep(3, 0);
assertItemInHistoryListStep(2, 1);
// Initial item is still in the queue.
assertItemInQueueListStep(1, 0);
}
[Test]
public void TestExpiredItemsMoveToQueueList()
{
addItemStep();
addItemStep();
AddStep("finish current item", () => Client.FinishCurrentItem());
assertItemInHistoryListStep(1, 0);
assertItemInQueueListStep(2, 0);
assertItemInQueueListStep(3, 1);
AddStep("finish current item", () => Client.FinishCurrentItem());
assertItemInHistoryListStep(2, 0);
assertItemInHistoryListStep(1, 1);
assertItemInQueueListStep(3, 0);
AddStep("finish current item", () => Client.FinishCurrentItem());
assertItemInHistoryListStep(3, 0);
assertItemInHistoryListStep(2, 1);
assertItemInHistoryListStep(1, 2);
}
[Test]
public void TestListsClearedWhenRoomLeft()
{
addItemStep();
AddStep("finish current item", () => Client.FinishCurrentItem());
AddStep("leave room", () => RoomManager.PartRoom());
AddUntilStep("wait for room part", () => Client.Room == null);
AddUntilStep("item 0 not in lists", () => !inHistoryList(0) && !inQueueList(0));
AddUntilStep("item 1 not in lists", () => !inHistoryList(0) && !inQueueList(0));
}
[Ignore("Expired items are initially removed from the room.")]
[Test]
public void TestJoinRoomWithMixedItemsAddedInCorrectLists()
{
AddStep("leave room", () => RoomManager.PartRoom());
AddUntilStep("wait for room part", () => Client.Room == null);
AddStep("join room with items", () =>
{
RoomManager.CreateRoom(new Room
{
Name = { Value = "test name" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
Ruleset = { Value = Ruleset.Value }
},
new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
Ruleset = { Value = Ruleset.Value },
Expired = true
}
}
});
});
AddUntilStep("wait for room join", () => RoomJoined);
assertItemInQueueListStep(1, 0);
assertItemInHistoryListStep(2, 0);
}
/// <summary>
/// Adds a step to create a new playlist item.
/// </summary>
private void addItemStep(bool expired = false) => AddStep("add item", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem
{
Beatmap = { Value = importedBeatmap },
BeatmapID = importedBeatmap.OnlineID ?? -1,
Expired = expired,
PlayedAt = DateTimeOffset.Now
})));
/// <summary>
/// Asserts the position of a given playlist item in the queue list.
/// </summary>
/// <param name="playlistItemId">The item id.</param>
/// <param name="visualIndex">The index at which the item should appear visually. The item with index 0 is at the top of the list.</param>
private void assertItemInQueueListStep(int playlistItemId, int visualIndex)
{
changeDisplayModeStep(MultiplayerPlaylistDisplayMode.Queue);
AddUntilStep($"{playlistItemId} in queue at pos = {visualIndex}", () =>
{
return !inHistoryList(playlistItemId)
&& this.ChildrenOfType<MultiplayerQueueList>()
.Single()
.ChildrenOfType<DrawableRoomPlaylistItem>()
.OrderBy(drawable => drawable.Position.Y)
.TakeWhile(drawable => drawable.Item.ID != playlistItemId)
.Count() == visualIndex;
});
}
/// <summary>
/// Asserts the position of a given playlist item in the history list.
/// </summary>
/// <param name="playlistItemId">The item id.</param>
/// <param name="visualIndex">The index at which the item should appear visually. The item with index 0 is at the top of the list.</param>
private void assertItemInHistoryListStep(int playlistItemId, int visualIndex)
{
changeDisplayModeStep(MultiplayerPlaylistDisplayMode.History);
AddUntilStep($"{playlistItemId} in history at pos = {visualIndex}", () =>
{
return !inQueueList(playlistItemId)
&& this.ChildrenOfType<MultiplayerHistoryList>()
.Single()
.ChildrenOfType<DrawableRoomPlaylistItem>()
.OrderBy(drawable => drawable.Position.Y)
.TakeWhile(drawable => drawable.Item.ID != playlistItemId)
.Count() == visualIndex;
});
}
private void changeDisplayModeStep(MultiplayerPlaylistDisplayMode mode) => AddStep($"change list to {mode}", () => list.DisplayMode.Value = mode);
private bool inQueueList(int playlistItemId)
{
return this.ChildrenOfType<MultiplayerQueueList>()
.Single()
.Items.Any(i => i.ID == playlistItemId);
}
private bool inHistoryList(int playlistItemId)
{
return this.ChildrenOfType<MultiplayerHistoryList>()
.Single()
.Items.Any(i => i.ID == playlistItemId);
}
}
}

View File

@ -0,0 +1,164 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using 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<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
[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<RearrangeableListItem<PlaylistItem>>().ElementAt(0).Model);
assertDeleteButtonVisibility(0, false);
assertDeleteButtonVisibility(1, true);
AddStep("select item 1", () => selectedItem.Value = playlist.ChildrenOfType<RearrangeableListItem<PlaylistItem>>().ElementAt(1).Model);
assertDeleteButtonVisibility(0, true);
assertDeleteButtonVisibility(1, false);
}
private void addPlaylistItem(Func<int> 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<RearrangeableListItem<PlaylistItem>>().Any(i => i.Model.ID == itemId));
}
private void deleteItem(int index)
{
OsuRearrangeableListItem<PlaylistItem> item = null;
AddStep($"move mouse to delete button {index}", () =>
{
item = playlist.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>>().ElementAt(index);
InputManager.MoveMouseTo(item.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(0));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddUntilStep("item removed from playlist", () => !playlist.ChildrenOfType<RearrangeableListItem<PlaylistItem>>().Contains(item));
}
private void assertDeleteButtonVisibility(int index, bool visible)
=> AddUntilStep($"delete button {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(index).Alpha > 0) == visible);
}
}

View File

@ -163,10 +163,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
addClickButtonStep();
AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
AddStep("transfer host", () => Client.TransferHost(Client.Room?.Users[1].UserID ?? 0));
addClickButtonStep();
AddAssert("match not started", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
AddUntilStep("user is idle (match not started)", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
AddAssert("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
}
[TestCase(true)]

View File

@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using 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
{

View File

@ -1,15 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using 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
{

View File

@ -0,0 +1,188 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.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<ScrollContainer<Drawable>>().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<DifficultyIcon>().ElementAt(index), offset));
private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () =>
{
var item = playlist.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>>().ElementAt(index);
InputManager.MoveMouseTo(item.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().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<PlaylistItem, RearrangeableListItem<PlaylistItem>> ItemMap => base.ItemMap;
public TestPlaylist()
{
AllowSelection = true;
}
}
}
}

View File

@ -24,9 +24,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestScenePlaylistsSongSelect : OnlinePlayTestScene
{
[Resolved]
private BeatmapManager beatmapManager { get; set; }
private BeatmapManager manager;
private RulesetStore rulesets;

View File

@ -11,6 +11,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@ -30,9 +31,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
private TestMultiplayerScreenStack multiplayerScreenStack;
private TestMultiplayerComponents multiplayerComponents;
private TestMultiplayerClient client => multiplayerScreenStack.Client;
private TestMultiplayerClient client => multiplayerComponents.Client;
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
@ -54,8 +55,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
});
AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack()));
AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded);
AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents()));
AddUntilStep("wait for multiplayer to load", () => multiplayerComponents.IsLoaded);
AddUntilStep("wait for lounge to load", () => this.ChildrenOfType<MultiplayerLoungeSubScreen>().FirstOrDefault()?.IsLoaded == true);
}
@ -76,7 +77,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
AddAssert("user state arrived", () => client.Room?.Users.FirstOrDefault()?.MatchState is TeamVersusUserState);
}
@ -102,7 +103,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press own button", () =>
{
InputManager.MoveMouseTo(multiplayerScreenStack.ChildrenOfType<TeamDisplay>().First());
InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType<TeamDisplay>().First());
InputManager.Click(MouseButton.Left);
});
AddAssert("user on team 1", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1);
@ -112,12 +113,39 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press other user's button", () =>
{
InputManager.MoveMouseTo(multiplayerScreenStack.ChildrenOfType<TeamDisplay>().ElementAt(1));
InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType<TeamDisplay>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddAssert("user still on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
}
[Test]
public void TestSettingsUpdatedWhenChangingMatchType()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Type = { Value = MatchType.HeadToHead },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddUntilStep("match type head to head", () => client.APIRoom?.Type.Value == MatchType.HeadToHead);
AddStep("change match type", () => client.ChangeSettings(new MultiplayerRoomSettings
{
MatchType = MatchType.TeamVersus
}));
AddUntilStep("api room updated to team versus", () => client.APIRoom?.Type.Value == MatchType.TeamVersus);
}
[Test]
public void TestChangeTypeViaMatchSettings()
{
@ -134,31 +162,32 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
AddAssert("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead);
AddUntilStep("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead);
AddUntilStep("team displays are not displaying teams", () => multiplayerScreenStack.ChildrenOfType<TeamDisplay>().All(d => d.DisplayedTeam == null));
AddUntilStep("team displays are not displaying teams", () => multiplayerComponents.ChildrenOfType<TeamDisplay>().All(d => d.DisplayedTeam == null));
AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus));
AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
AddUntilStep("team displays are displaying teams", () => multiplayerScreenStack.ChildrenOfType<TeamDisplay>().All(d => d.DisplayedTeam != null));
AddUntilStep("team displays are displaying teams", () => multiplayerComponents.ChildrenOfType<TeamDisplay>().All(d => d.DisplayedTeam != null));
}
private void createRoom(Func<Room> room)
{
AddStep("open room", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().Single().Open(room()));
AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open(room()));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2);
AddUntilStep("create room button enabled", () => this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single().Enabled.Value);
AddStep("create room", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for join", () => multiplayerScreenStack.RoomManager.RoomJoined);
AddUntilStep("wait for join", () => client.RoomJoined);
}
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Overlays.Settings.Sections;
using osu.Game.Skinning;
using osu.Game.Skinning.Editor;
namespace osu.Game.Tests.Visual.Navigation
{
public class TestSceneEditDefaultSkin : OsuGameTestScene
{
private SkinManager skinManager => Game.Dependencies.Get<SkinManager>();
private SkinEditorOverlay skinEditor => Game.Dependencies.Get<SkinEditorOverlay>();
[Test]
public void TestEditDefaultSkin()
{
AddAssert("is default skin", () => skinManager.CurrentSkinInfo.Value.ID == SkinInfo.DEFAULT_SKIN);
AddStep("open settings", () => { Game.Settings.Show(); });
// Until step requires as settings has a delayed load.
AddUntilStep("export button disabled", () => Game.Settings.ChildrenOfType<SkinSection.ExportSkinButton>().SingleOrDefault()?.Enabled.Value == false);
// Will create a mutable skin.
AddStep("open skin editor", () => skinEditor.Show());
// Until step required as the skin editor may take time to load (and an extra scheduled frame for the mutable part).
AddUntilStep("is modified default skin", () => skinManager.CurrentSkinInfo.Value.ID != SkinInfo.DEFAULT_SKIN);
AddAssert("is not protected", () => skinManager.CurrentSkinInfo.Value.PerformRead(s => !s.Protected));
AddUntilStep("export button enabled", () => Game.Settings.ChildrenOfType<SkinSection.ExportSkinButton>().SingleOrDefault()?.Enabled.Value == true);
}
}
}

View File

@ -0,0 +1,99 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Configuration;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Navigation
{
public class TestSceneMouseWheelVolumeAdjust : OsuGameTestScene
{
public override void SetUpSteps()
{
base.SetUpSteps();
// Headless tests are always at minimum volume. This covers interactive tests, matching that initial value.
AddStep("Set volume to min", () => Game.Audio.Volume.Value = 0);
AddAssert("Volume is min", () => Game.Audio.AggregateVolume.Value == 0);
AddStep("Move mouse to centre", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
}
[Test]
public void TestAdjustVolumeFromMainMenu()
{
// First scroll makes volume controls appear, second adjusts volume.
AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2);
AddUntilStep("Volume is above zero", () => Game.Audio.AggregateVolume.Value > 0);
}
[Test]
public void TestAdjustVolumeFromPlayerWheelEnabled()
{
loadToPlayerNonBreakTime();
// First scroll makes volume controls appear, second adjusts volume.
AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2);
AddAssert("Volume is above zero", () => Game.Audio.Volume.Value > 0);
}
[Test]
public void TestAdjustVolumeFromPlayerWheelDisabled()
{
AddStep("disable wheel volume adjust", () => Game.LocalConfig.SetValue(OsuSetting.MouseDisableWheel, true));
loadToPlayerNonBreakTime();
// First scroll makes volume controls appear, second adjusts volume.
AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2);
AddAssert("Volume is still zero", () => Game.Audio.Volume.Value == 0);
}
[Test]
public void TestAdjustVolumeFromPlayerWheelDisabledHoldingAlt()
{
AddStep("disable wheel volume adjust", () => Game.LocalConfig.SetValue(OsuSetting.MouseDisableWheel, true));
loadToPlayerNonBreakTime();
// First scroll makes volume controls appear, second adjusts volume.
AddRepeatStep("Adjust volume using mouse wheel holding alt", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.ScrollVerticalBy(5);
InputManager.ReleaseKey(Key.AltLeft);
}, 2);
AddAssert("Volume is above zero", () => Game.Audio.Volume.Value > 0);
}
private void loadToPlayerNonBreakTime()
{
Player player = null;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSceneScreenNavigation.TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for player", () =>
{
// dismiss any notifications that may appear (ie. muted notification).
clickMouseInCentre();
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
});
AddUntilStep("wait for play time active", () => !player.IsBreakTime.Value);
}
private void clickMouseInCentre()
{
InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Left);
}
}
}

View File

@ -83,9 +83,6 @@ namespace osu.Game.Tests.Visual.Navigation
[Resolved]
private OsuGameBase gameBase { get; set; }
[Resolved]
private GameHost host { get; set; }
[Test]
public void TestNullRulesetHandled()
{

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