1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-13 22:43:24 +08:00

Compare commits

...

78 Commits

240 changed files with 5755 additions and 2476 deletions
+8 -11
View File
@@ -3,28 +3,25 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2023.3.3",
"version": "2025.2.3",
"commands": [
"jb"
]
},
"nvika": {
"version": "4.0.0",
"commands": [
"nvika"
]
],
"rollForward": false
},
"codefilesanity": {
"version": "0.0.37",
"commands": [
"CodeFileSanity"
]
],
"rollForward": false
},
"ppy.localisationanalyser.tools": {
"version": "2025.1208.0",
"commands": [
"localisation"
]
],
"rollForward": false
}
}
}
}
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
steps:
- name: Checkout diffcalc-sheet-generator
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
path: ${{ inputs.id }}
repository: 'smoogipoo/diffcalc-sheet-generator'
+50 -17
View File
@@ -6,6 +6,7 @@ concurrency:
permissions:
contents: read # to fetch code (actions/checkout)
security-events: write # for reporting InspectCode issues
jobs:
inspect-code:
@@ -13,10 +14,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: "8.0.x"
@@ -27,7 +28,7 @@ jobs:
run: dotnet restore osu.Desktop.slnf
- name: Restore inspectcode cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ github.workspace }}/inspectcode
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', 'osu*.slnf', '.editorconfig', '.globalconfig', 'CodeAnalysis/*', '**/*.csproj', '**/*.props') }}
@@ -49,10 +50,14 @@ jobs:
exit $exit_code
- name: InspectCode
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
- name: NVika
run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
uses: JetBrains/ReSharper-InspectCode@v0.12
with:
# this is WTF tier but if you don't specify *both* of these the defaults assume `build: true`
build: false
no-build: true
solution: ./osu.Desktop.slnf
caches-home: inspectcode
verbosity: WARN
test:
name: Test
@@ -71,10 +76,10 @@ jobs:
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: "8.0.x"
@@ -96,30 +101,58 @@ jobs:
NUnit.ConsoleOut=0
# Attempt to upload results even if test fails.
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
# https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#cancelled
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: ${{ always() }}
uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
test-results:
name: Test results
runs-on: ubuntu-latest
# we want to wait for the `test` job to complete, but run regardless of whether it succeeds or fails
# https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#example-not-requiring-successful-dependent-jobs
if: ${{ !cancelled() }}
needs: test
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download results
uses: actions/download-artifact@v8
with:
pattern: osu-test-results-*
merge-multiple: true
- name: Add test results summary to workflow run
uses: dorny/test-reporter@v3.0.0
with:
name: Results
path: "*.trx"
reporter: dotnet-trx
list-suites: 'failed'
list-tests: 'failed'
use-actions-summary: 'true'
build-only-android:
name: Build only (Android)
runs-on: windows-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup JDK 11
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: microsoft
java-version: 11
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: "8.0.x"
@@ -135,10 +168,10 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: "8.0.x"
+5 -4
View File
@@ -3,7 +3,8 @@ name: Pack and nuget
on:
push:
tags:
- '*'
- '*.*.*'
- '!*-*'
jobs:
notify_pending_production_deploy:
@@ -43,14 +44,14 @@ jobs:
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set artifacts directory
id: artifactsPath
run: echo "::set-output name=nuget_artifacts::${{github.workspace}}/artifacts"
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: "8.0.x"
@@ -76,7 +77,7 @@ jobs:
dotnet pack -c Release Templates /p:Version=${{ github.ref_name }} -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
- name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: osu
path: |
-45
View File
@@ -1,45 +0,0 @@
# This is a workaround to allow PRs to report their coverage. This will run inside the base repository.
# See:
# * https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories
# * https://docs.github.com/en/actions/reference/authentication-in-a-workflow#permissions-for-the-github_token
name: Annotate CI run with test results
on:
workflow_run:
workflows: [ "Continuous Integration" ]
types:
- completed
permissions:
contents: read
actions: read
checks: write
jobs:
annotate:
name: Annotate CI run with test results
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
with:
repository: ${{ github.event.workflow_run.repository.full_name }}
ref: ${{ github.event.workflow_run.head_sha }}
- name: Download results
uses: actions/download-artifact@v4
with:
pattern: osu-test-results-*
merge-multiple: true
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Annotate CI run with test results
uses: dorny/test-reporter@v1.8.0
with:
name: Results
path: "*.trx"
reporter: dotnet-trx
list-suites: 'failed'
list-tests: 'failed'
+2 -2
View File
@@ -13,12 +13,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Create Sentry release
uses: getsentry/action-release@v1
uses: getsentry/action-release@v3
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ppy
+13
View File
@@ -13,6 +13,19 @@
"preLaunchTask": "Build osu! (Debug)",
"console": "internalConsole"
},
{
"name": "osu! (Debug, Second Client)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Desktop/bin/Debug/net8.0/osu!.dll",
"--debug-client-id=1"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Debug)",
"console": "internalConsole"
},
{
"name": "osu! (Release)",
"type": "coreclr",
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.310.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.318.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
+1
View File
@@ -58,6 +58,7 @@ namespace osu.Desktop
private readonly RichPresence presence = new RichPresence
{
Assets = new Assets { LargeImageKey = "osu_logo_lazer" },
Timestamps = Timestamps.Now,
Secrets = new Secrets
{
JoinSecret = null,
@@ -0,0 +1,55 @@
// 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.IO;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
namespace osu.Desktop.MacOS
{
/// <summary>
/// Checks if the game is located at `Applications` folder and displays a warning notification if not so.
/// </summary>
public partial class MacOSAppLocationChecker : Component
{
[Resolved]
private INotificationOverlay notification { get; set; } = null!;
protected override void LoadComplete()
{
base.LoadComplete();
string assemblyPath = RuntimeInfo.EntryAssembly.Location;
bool inRootApp = assemblyPath.StartsWith("/Applications/", StringComparison.Ordinal);
bool inUserApp = assemblyPath.StartsWith(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Applications/"), StringComparison.Ordinal);
if (!inRootApp && !inUserApp)
notification.Post(new MacOSAppLocationNotification());
Expire();
}
private partial class MacOSAppLocationNotification : SimpleNotification
{
public MacOSAppLocationNotification()
{
Text = NotificationsStrings.MacOSAppLocation(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Icon = FontAwesome.Solid.ShieldAlt;
IconContent.Colour = colours.YellowDark;
}
}
}
}
+13 -5
View File
@@ -5,7 +5,6 @@ using System;
using System.IO;
using System.Reflection;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Microsoft.Win32;
using osu.Desktop.Performance;
using osu.Desktop.Security;
@@ -15,12 +14,12 @@ using osu.Desktop.Updater;
using osu.Framework;
using osu.Framework.Logging;
using osu.Game.Updater;
using osu.Desktop.MacOS;
using osu.Desktop.Windows;
using osu.Framework.Allocation;
using osu.Game.Configuration;
using osu.Game.IO;
using osu.Game.IPC;
using osu.Game.Online.Multiplayer;
using osu.Game.Performance;
using osu.Game.Utils;
@@ -123,7 +122,7 @@ namespace osu.Desktop
public override bool RestartAppWhenExited()
{
Task.Run(() => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId)).FireAndForget();
RestartOnExitAction = () => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId);
return true;
}
@@ -133,8 +132,17 @@ namespace osu.Desktop
LoadComponentAsync(new DiscordRichPresence(), Add);
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
break;
case RuntimeInfo.Platform.macOS when !IsPackageManaged && IsDeployedBuild:
if (!IsPackageManaged && IsDeployedBuild)
LoadComponentAsync(new MacOSAppLocationChecker(), Add);
break;
}
LoadComponentAsync(new ElevatedPrivilegesChecker(), Add);
@@ -27,6 +27,8 @@ namespace osu.Desktop.Security
if (Environment.IsPrivilegedProcess)
notifications.Post(new ElevatedPrivilegesNotification());
Expire();
}
private partial class ElevatedPrivilegesNotification : SimpleNotification
+4 -4
View File
@@ -146,11 +146,11 @@ namespace osu.Desktop.Updater
action();
}
private void restartToApplyUpdate(Velopack.UpdateManager updateManager, UpdateInfo update) => Task.Run(async () =>
private void restartToApplyUpdate(Velopack.UpdateManager updateManager, UpdateInfo update)
{
await updateManager.WaitExitThenApplyUpdatesAsync(update.TargetFullRelease).ConfigureAwait(false);
Schedule(() => game.AttemptExit());
});
game.RestartOnExitAction = () => updateManager.WaitExitThenApplyUpdates(update.TargetFullRelease);
game.AttemptExit();
}
private static void log(string text) => Logger.Log($"VelopackUpdateManager: {text}");
}
@@ -193,20 +193,20 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public Color4 HyperDashColour
{
get => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()];
set => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = value;
get => Configuration.CustomColours[nameof(CatchSkinColour.HyperDash)];
set => Configuration.CustomColours[nameof(CatchSkinColour.HyperDash)] = value;
}
public Color4 HyperDashAfterImageColour
{
get => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()];
set => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = value;
get => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashAfterImage)];
set => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashAfterImage)] = value;
}
public Color4 HyperDashFruitColour
{
get => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()];
set => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = value;
get => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashFruit)];
set => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashFruit)] = value;
}
public TestSkin()
+1
View File
@@ -155,6 +155,7 @@ namespace osu.Game.Rulesets.Catch
new CatchModMuted(),
new CatchModNoScope(),
new CatchModMovingFast(),
new CatchModSynesthesia(),
};
case ModType.System:
@@ -0,0 +1,60 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Screens.Edit;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Mods
{
/// <summary>
/// Mod that colours <see cref="HitObject"/>s based on the musical division they are on
/// </summary>
public class CatchModSynesthesia : ModSynesthesia, IApplicableToBeatmap, IApplicableToDrawableHitObject
{
private readonly OsuColour colours = new OsuColour();
private IBeatmap? currentBeatmap { get; set; }
public void ApplyToBeatmap(IBeatmap beatmap)
{
//Store a reference to the current beatmap to look up the beat divisor when notes are drawn
if (currentBeatmap != beatmap)
currentBeatmap = beatmap;
}
public void ApplyToDrawableHitObject(DrawableHitObject d)
{
if (currentBeatmap == null) return;
Color4? timingBasedColour = null;
d.HitObjectApplied += _ =>
{
// Block bananas from getting coloured.
if (d.HitObject is not Banana)
{
timingBasedColour = BindableBeatDivisor.GetColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(d.HitObject.StartTime), colours);
}
// Colour droplets into a solid colour, as droplets aren't generated snapped to timeline ticks.
if (d.HitObject is Droplet)
{
timingBasedColour = Color4.LightGreen;
}
};
// Need to set this every update to ensure it doesn't get overwritten by DrawableHitObject.OnApply() -> UpdateComboColour().
d.OnUpdate += _ =>
{
if (timingBasedColour != null)
d.AccentColour.Value = timingBasedColour.Value;
};
}
}
}
@@ -10,6 +10,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
@@ -505,6 +506,29 @@ namespace osu.Game.Rulesets.Mania.Tests
.All(j => j.Type.IsHit()));
}
/// <summary>
/// This ensures that the value of <see cref="DrawableHoldNote.MissingStartTime"/>
/// will be set correctly when the body receives a judgment during the hold.
///
/// -----[ ]-----
/// x o
/// </summary>
[Test]
public void TestReleaseDuringHoldMissingStartTime()
{
performTest([
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_during_hold_1)
]);
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Miss);
assertNoteJudgement(HitResult.IgnoreMiss);
AddAssert("body judgement is miss", () => !judgementResults.Single(j => j.HitObject is HoldNoteBody).IsHit);
AddAssert("body judgement time indicates during hold", () => judgementResults.Single(j => j.HitObject is HoldNoteBody).TimeAbsolute, () => Is.EqualTo(time_during_hold_1).Within(100));
}
private void assertHitObjectJudgement(HitObject hitObject, HitResult result)
=> AddAssert($"object judged as {result}", () => judgementResults.First(j => j.HitObject == hitObject).Type, () => Is.EqualTo(result));
@@ -31,10 +31,21 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
public override bool DisplayResult => false;
/// <summary>
/// Whether the user is currently pressing the hold note.
/// </summary>
public IBindable<bool> IsHolding => isHolding;
private readonly Bindable<bool> isHolding = new Bindable<bool>();
/// <summary>
/// The time at which the user starting missing the hold note.
/// This could be the time at which they missed the head, broke on the body, or missed the tail.
/// </summary>
public IBindable<double?> MissingStartTime => missingStartTime;
private readonly Bindable<double?> missingStartTime = new Bindable<double?>();
public DrawableHoldNoteHead Head => headContainer.Child;
public DrawableHoldNoteTail Tail => tailContainer.Child;
public DrawableHoldNoteBody Body => bodyContainer.Child;
@@ -197,11 +208,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public override void OnKilled()
{
base.OnKilled();
// flush the final state of holding on kill.
// this matters because some skin implementations like legacy skin
// insert drawables in the hierarchy that are not a child of this DHO
// (see `LegacyBodyPiece` and related machinations with `lightContainer` being added at column level)
isHolding.Value = Result.IsHolding(Time.Current);
missingStartTime.Value = null;
(bodyPiece.Drawable as IHoldNoteBody)?.Recycle();
}
@@ -209,6 +222,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
base.Update();
if (Head.Judged && !Head.IsHit)
missingStartTime.Value ??= Head.Result.TimeAbsolute;
if (Body.HasHoldBreak)
missingStartTime.Value ??= Body.Result.TimeAbsolute;
if (Tail.Judged && !Tail.IsHit)
missingStartTime.Value ??= Tail.Result.TimeAbsolute;
isHolding.Value = Result.IsHolding(Time.Current);
// Pad the full size container so its contents (i.e. the masking container) reach under the tail.
@@ -3,6 +3,7 @@
#nullable disable
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Objects.Drawables;
@@ -14,6 +15,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
public partial class DrawableHoldNoteHead : DrawableNote
{
/// <summary>
/// The time at which the user starting missing the hold note.
/// This could be the time at which they missed the head, broke on the body, or missed the tail.
/// </summary>
public readonly IBindable<double?> MissingStartTime = new Bindable<double?>();
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteHead;
public DrawableHoldNoteHead()
@@ -28,6 +35,22 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Origin = Anchor.TopCentre;
}
protected override void OnApply()
{
base.OnApply();
if (ParentHitObject is DrawableHoldNote parentHold)
MissingStartTime.BindTo(parentHold.MissingStartTime);
}
protected override void OnFree()
{
base.OnFree();
if (ParentHitObject is DrawableHoldNote parentHold)
MissingStartTime.UnbindFrom(parentHold.MissingStartTime);
}
public bool UpdateResult() => base.UpdateResult(true);
protected override void UpdateHitStateTransforms(ArmedState state)
@@ -3,6 +3,7 @@
#nullable disable
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Scoring;
@@ -14,6 +15,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
public partial class DrawableHoldNoteTail : DrawableNote
{
/// <summary>
/// The time at which the user starting missing the hold note.
/// This could be the time at which they missed the head, broke on the body, or missed the tail.
/// </summary>
public readonly IBindable<double?> MissingStartTime = new Bindable<double?>();
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
protected internal DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
@@ -30,6 +37,22 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Origin = Anchor.TopCentre;
}
protected override void OnApply()
{
base.OnApply();
if (ParentHitObject is DrawableHoldNote parentHold)
MissingStartTime.BindTo(parentHold.MissingStartTime);
}
protected override void OnFree()
{
base.OnFree();
if (ParentHitObject is DrawableHoldNote parentHold)
MissingStartTime.UnbindFrom(parentHold.MissingStartTime);
}
public void UpdateResult() => base.UpdateResult(true);
protected override void CheckForResult(bool userTriggered, double timeOffset) =>
+2 -1
View File
@@ -118,7 +118,8 @@ namespace osu.Game.Rulesets.Mania.Objects
AddNested(Body = new HoldNoteBody
{
StartTime = StartTime,
Column = Column
Column = Column,
Duration = Duration
});
}
@@ -3,6 +3,7 @@
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects
@@ -13,9 +14,11 @@ namespace osu.Game.Rulesets.Mania.Objects
/// On hit - the hold note was held correctly for the full duration.<br />
/// On miss - the hold note was released at some point during its judgement period.
/// </summary>
public class HoldNoteBody : ManiaHitObject
public class HoldNoteBody : ManiaHitObject, IHasDuration
{
public override Judgement CreateJudgement() => new HoldNoteBodyJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public double Duration { get; set; }
public double EndTime => StartTime + Duration;
}
}
@@ -25,17 +25,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly IBindable<bool> isHitting = new Bindable<bool>();
/// <summary>
/// Stores the start time of the fade animation that plays when any of the nested
/// hitobjects of the hold note are missed.
/// </summary>
private readonly Bindable<double?> missFadeTime = new Bindable<double?>();
private readonly IBindable<double?> missingStartTime = new Bindable<double?>();
private Drawable? bodySprite;
private Drawable? lightContainer;
private Drawable? light;
private LegacyNoteBodyStyle? bodyStyle;
@@ -87,6 +80,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
direction.BindTo(scrollingInfo.Direction);
isHitting.BindTo(holdNote.IsHolding);
missingStartTime.BindTo(holdNote.MissingStartTime);
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30)?.With(d =>
{
@@ -109,26 +103,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
direction.BindValueChanged(onDirectionChanged, true);
isHitting.BindValueChanged(onIsHittingChanged, true);
missFadeTime.BindValueChanged(onMissFadeTimeChanged, true);
missingStartTime.BindValueChanged(onMissingStartTimeChanged, true);
holdNote.ApplyCustomUpdateState += applyCustomUpdateState;
applyCustomUpdateState(holdNote, holdNote.State.Value);
}
private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state)
{
switch (hitObject)
{
// Ensure that the hold note is also faded out when the head/tail/body is missed.
// Importantly, we filter out unrelated objects like DrawableNotePerfectBonus.
case DrawableHoldNoteTail:
case DrawableHoldNoteHead:
case DrawableHoldNoteBody:
if (state == ArmedState.Miss)
missFadeTime.Value ??= hitObject.HitStateUpdateTime;
break;
}
holdNote.ApplyCustomUpdateState += onApplyCustomUpdateState;
}
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
@@ -187,23 +164,19 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
}
}
private void onMissFadeTimeChanged(ValueChangedEvent<double?> missFadeTimeChange)
private void onMissingStartTimeChanged(ValueChangedEvent<double?> startTime)
=> applyMissingDim();
private void onApplyCustomUpdateState(DrawableHitObject obj, ArmedState state)
=> applyMissingDim();
private void applyMissingDim()
{
if (missFadeTimeChange.NewValue == null)
if (missingStartTime.Value == null)
return;
// this update could come from any nested object of the hold note (or even from an input).
// make sure the transforms are consistent across all affected parts.
using (BeginAbsoluteSequence(missFadeTimeChange.NewValue.Value))
{
// colour and duration matches stable
// transforms not applied to entire hold note in order to not affect hit lighting
const double fade_duration = 60;
holdNote.Head.FadeColour(Colour4.DarkGray, fade_duration);
holdNote.Tail.FadeColour(Colour4.DarkGray, fade_duration);
bodySprite?.FadeColour(Colour4.DarkGray, fade_duration);
}
using (BeginAbsoluteSequence(missingStartTime.Value.Value))
this.FadeColour(Colour4.DarkGray, 60);
}
protected override void Update()
@@ -213,9 +186,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (!isHitting.Value)
(bodySprite as TextureAnimation)?.GotoFrame(0);
if (holdNote.Body.HasHoldBreak)
missFadeTime.Value = holdNote.Body.Result.TimeAbsolute;
int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1);
// here we go...
@@ -251,7 +221,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
base.Dispose(isDisposing);
if (holdNote.IsNotNull())
holdNote.ApplyCustomUpdateState -= applyCustomUpdateState;
holdNote.ApplyCustomUpdateState -= onApplyCustomUpdateState;
lightContainer?.Expire();
}
@@ -1,18 +1,61 @@
// 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.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public partial class LegacyHoldNoteHeadPiece : LegacyNotePiece
{
private readonly IBindable<double?> missingStartTime = new Bindable<double?>();
[Resolved]
private DrawableHitObject drawableObject { get; set; } = null!;
protected override void LoadComplete()
{
base.LoadComplete();
missingStartTime.BindTo(((DrawableHoldNoteHead)drawableObject).MissingStartTime);
missingStartTime.BindValueChanged(onMissingStartTimeChanged, true);
drawableObject.ApplyCustomUpdateState += onApplyCustomUpdateState;
}
private void onMissingStartTimeChanged(ValueChangedEvent<double?> startTime)
=> applyMissingDim();
private void onApplyCustomUpdateState(DrawableHitObject obj, ArmedState state)
=> applyMissingDim();
private void applyMissingDim()
{
if (missingStartTime.Value == null)
return;
using (BeginAbsoluteSequence(missingStartTime.Value.Value))
this.FadeColour(Colour4.DarkGray, 60);
}
protected override Drawable? GetAnimation(ISkinSource skin)
{
// TODO: Should fallback to the head from default legacy skin instead of note.
return GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage)
?? GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableObject.IsNotNull())
drawableObject.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
}
}
@@ -1,8 +1,12 @@
// 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.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@@ -10,6 +14,36 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public partial class LegacyHoldNoteTailPiece : LegacyNotePiece
{
private readonly IBindable<double?> missingStartTime = new Bindable<double?>();
[Resolved]
private DrawableHitObject drawableObject { get; set; } = null!;
protected override void LoadComplete()
{
base.LoadComplete();
missingStartTime.BindTo(((DrawableHoldNoteTail)drawableObject).MissingStartTime);
missingStartTime.BindValueChanged(onMissingStartTimeChanged, true);
drawableObject.ApplyCustomUpdateState += onApplyCustomUpdateState;
}
private void onMissingStartTimeChanged(ValueChangedEvent<double?> startTime)
=> applyMissingDim();
private void onApplyCustomUpdateState(DrawableHitObject obj, ArmedState state)
=> applyMissingDim();
private void applyMissingDim()
{
if (missingStartTime.Value == null)
return;
using (BeginAbsoluteSequence(missingStartTime.Value.Value))
this.FadeColour(Colour4.DarkGray, 60);
}
protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{
// Invert the direction
@@ -25,5 +59,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
?? GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage)
?? GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableObject.IsNotNull())
drawableObject.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
}
}
@@ -9,9 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Input;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Handlers;
@@ -63,16 +61,11 @@ namespace osu.Game.Rulesets.Mania.UI
public double TargetTimeRange { get; protected set; }
private double currentTimeRange;
// Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
private ISkinSource currentSkin = null!;
[Resolved]
private GameHost gameHost { get; set; } = null!;
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
: base(ruleset, beatmap, mods)
{
@@ -119,7 +112,7 @@ namespace osu.Game.Rulesets.Mania.UI
TargetTimeRange = ComputeScrollTime(speed.NewValue);
});
TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
TimeRange.Value = TargetTimeRange = ComputeScrollTime(configScrollSpeed.Value);
Config.BindWith(ManiaRulesetSetting.MobileLayout, mobileLayout);
mobileLayout.BindValueChanged(_ => updateMobileLayout(), true);
@@ -179,9 +172,7 @@ namespace osu.Game.Rulesets.Mania.UI
// This scaling factor preserves the scroll speed as the scroll length varies from changes to the hit position.
float scale = lengthToHitPosition / length_to_default_hit_position;
// we're intentionally using the game host's update clock here to decouple the time range tween from the gameplay clock (which can be arbitrarily paused, or even rewinding)
currentTimeRange = Interpolation.DampContinuously(currentTimeRange, TargetTimeRange, 50, gameHost.UpdateThread.Clock.ElapsedFrameTime);
TimeRange.Value = currentTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale;
TimeRange.Value = TargetTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale;
}
/// <summary>
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Vector2 oldStartPos = default;
Vector2 oldEndPos = default;
double oldDistance = default;
double oldDistance = 0;
var oldControlPointTypes = controlPoints.Select(p => p.Type);
AddStep("Add slider", () =>
@@ -255,7 +255,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Vector2 oldStartPos = default;
Vector2 oldEndPos = default;
double oldDistance = default;
double oldDistance = 0;
var oldControlPointTypes = segmentedSliderPath.Select(p => p.Type);
@@ -141,6 +141,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary>
public void MissForcefully() => ApplyMinResult();
// ReSharper disable once FunctionRecursiveOnAllPaths (TODO: remove after fixed https://youtrack.jetbrains.com/issue/RIDER-135036/Incorrect-recursive-on-all-execution-paths-inspection)
private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent!.ScreenSpaceDrawQuad.AABBFloat;
/// <summary>
@@ -9,12 +9,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
public partial class TrianglesPiece : Triangles
{
protected override bool CreateNewTriangles => false;
protected override float SpawnRatio => 0.5f;
public TrianglesPiece(int? seed = null)
: base(seed)
{
TriangleScale = 1.2f;
SpawnRatio = 0.5f;
HideAlphaDiscrepancies = false;
ClampAxes = Axes.None;
}
@@ -1,7 +1,9 @@
// 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.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -24,6 +26,36 @@ namespace osu.Game.Tests.Beatmaps.IO
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
[TestCase(96, 32)]
[TestCase(32, 32)]
[TestCase(24, 32)]
[TestCase(23, 16)]
[TestCase(15, 16)]
[TestCase(12, 16)]
[TestCase(11, 8)]
[TestCase(6, 8)]
[TestCase(2, 4)]
public void TestGridSizeClampedToStableValues(int set, int expected)
{
IWorkingBeatmap beatmap = null!;
MemoryStream outStream = null!;
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"decimal-timing-beatmap.olz"));
AddStep("adjust grid", () => beatmap.Beatmap.GridSize = set);
AddStep("save", () => beatmaps.Save((beatmap.BeatmapInfo as BeatmapInfo)!, beatmap.Beatmap));
AddStep("export", () =>
{
outStream = new MemoryStream();
new LegacyBeatmapExporter(LocalStorage)
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
});
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
AddAssert("grid clamped", () => beatmap.Beatmap.GridSize, () => Is.EqualTo(expected));
}
[Test]
public void TestObjectsSnappedAfterTruncatingExport()
{
@@ -131,6 +163,48 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
[Test]
public void TestExportUsesCarriageReturnLineFeed()
{
IWorkingBeatmap beatmap = null!;
MemoryStream outStream = null!;
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"legacy-export-stability-test.olz"));
AddStep("export", () =>
{
outStream = new MemoryStream();
new LegacyBeatmapExporter(LocalStorage)
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
});
AddAssert(".osu file uses CRLF line endings",
() => hasBareLineFeed(outStream.GetBuffer()),
() => Is.False);
bool hasBareLineFeed(byte[] archiveBytes)
{
using var memoryStream = new MemoryStream(archiveBytes);
using var archiveReader = new ZipArchiveReader(memoryStream);
foreach (string filename in archiveReader.Filenames.Where(f => f.EndsWith(".osu", StringComparison.Ordinal)))
{
byte[] content = archiveReader.GetStream(filename).ReadAllBytesToArray();
for (int i = 0; i < content.Length; i++)
{
if (content[i] != '\n')
continue;
if (i == 0 || content[i - 1] != '\r')
return true;
}
}
return false;
}
}
private IWorkingBeatmap importBeatmapFromStream(Stream stream)
{
var imported = beatmaps.Import(new ImportTask(stream, "filename.osz")).GetResultSafely();
@@ -65,6 +65,7 @@ namespace osu.Game.Tests.Beatmaps
}
[Test]
[FlakyTest] // one fix attempted in https://github.com/ppy/osu/pull/37178, didn't work
public void TestInvalidationFlow()
{
BeatmapInfo postEditBeatmapInfo = null;
@@ -187,6 +187,7 @@ namespace osu.Game.Tests.Database
}
[Test]
[FlakyTest]
public void TestCustomRulesetScoreNotSubjectToUpgrades([Values] bool available)
{
RulesetInfo rulesetInfo = null!;
@@ -258,6 +258,6 @@ namespace osu.Game.Tests.Gameplay
}
private void disableLayeredHitSounds()
=> AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[LegacySetting.LayeredHitSounds.ToString()] = "0");
=> AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[nameof(LegacySetting.LayeredHitSounds)] = "0");
}
}
@@ -219,6 +219,46 @@ namespace osu.Game.Tests.NonVisual.Filtering
ClassicAssert.AreEqual(filtered, carouselItem.Filtered.Value);
}
[Test]
[TestCase("\"quoted words\"", false)]
[TestCase("\"the artist\"", false)]
[TestCase("the artist \"quoted words\"", false)]
[TestCase("\"unknown\"", true)]
public void TestCriteriaMatchingTermsAdjacentToPunctuation(string terms, bool filtered)
{
var exampleBeatmapInfo = getExampleBeatmap();
exampleBeatmapInfo.Metadata.Title = "the artist \"quoted words\"";
var criteria = new FilterCriteria
{
Ruleset = new RulesetInfo { OnlineID = 6 },
AllowConvertedBeatmaps = true,
SearchText = terms
};
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
carouselItem.Filter(criteria);
ClassicAssert.AreEqual(filtered, carouselItem.Filtered.Value);
}
[Test]
[TestCase("~quoted words~", false)]
[TestCase("the artist", false)]
[TestCase("the artist ~quoted words~", false)]
[TestCase("~unknown~", true)]
public void TestCriteriaMatchingTermsAdjacentToMathSymbols(string terms, bool filtered)
{
var exampleBeatmapInfo = getExampleBeatmap();
exampleBeatmapInfo.Metadata.Title = "the artist ~quoted words~";
var criteria = new FilterCriteria
{
Ruleset = new RulesetInfo { OnlineID = 6 },
AllowConvertedBeatmaps = true,
SearchText = terms
};
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
carouselItem.Filter(criteria);
ClassicAssert.AreEqual(filtered, carouselItem.Filtered.Value);
}
[Test]
[TestCase("", false)]
[TestCase("Goes", false)]
@@ -25,6 +25,7 @@ namespace osu.Game.Tests.Visual.Components
private NotificationOverlay notificationOverlay = null!;
private ChatOverlay chatOverlay = null!;
private TestMetadataClient metadataClient = null!;
private FriendPresenceNotifier notifier = null!;
[SetUp]
public void Setup() => Schedule(() =>
@@ -45,7 +46,11 @@ namespace osu.Game.Tests.Visual.Components
notificationOverlay,
chatOverlay,
metadataClient,
new FriendPresenceNotifier()
notifier = new FriendPresenceNotifier
{
// Speeds up tests that don't rely on this debounce a little bit.
OfflineDebounceTime = 0
}
}
};
@@ -127,5 +132,27 @@ namespace osu.Game.Tests.Visual.Components
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2));
}
[Test]
public void TestOfflineDebounce()
{
AddStep("set debounce time", () =>
{
notifier.NotificationDebounceTime = 0;
notifier.OfflineDebounceTime = 5000;
});
AddStep("bring friend online", () => metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }));
AddUntilStep("online notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
for (int i = 0; i < 3; i++)
{
AddStep("bring friend online", () => metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }));
AddStep("bring friend offline", () => metadataClient.FriendPresenceUpdated(1, null));
}
AddUntilStep("online notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
AddUntilStep("offline notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2));
}
}
}
@@ -128,6 +128,7 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
[FlakyTest]
public void TestLengthAndStarRatingUpdated()
{
WorkingBeatmap working = null;
@@ -24,6 +24,7 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
[FlakyTest]
public void TestLocallyModifyingOnlineBeatmap()
{
string initialHash = string.Empty;
@@ -4,7 +4,6 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
@@ -140,14 +139,16 @@ namespace osu.Game.Tests.Visual.Editing
private void setUpEditor(RulesetInfo ruleset)
{
BeatmapSetInfo beatmapSet = null!;
BeatmapSetInfo? beatmapSet = null;
AddStep("Import test beatmap", () =>
Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()
);
AddStep("Retrieve beatmap", () =>
beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()
);
AddUntilStep("Retrieve beatmap", () =>
{
beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected)?.Value.Detach();
return beatmapSet != null;
});
AddStep("Present beatmap", () => Game.PresentBeatmap(beatmapSet));
AddUntilStep("Wait for song select", () =>
Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
@@ -157,7 +158,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset);
AddStep("Open editor for ruleset", () =>
((SoloSongSelect)Game.ScreenStack.CurrentScreen)
.Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name))
.Edit(beatmapSet!.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name))
);
AddUntilStep("Wait for editor open", () => editor?.ReadyForUse == true);
}
@@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private bool seek;
[Test]
[FlakyTest]
[Ignore("Still failing even with [FlakyTest] applied.")]
public void TestAllSamplesStopDuringSeek()
{
DrawableSlider? slider = null;
@@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("move mouse to centre of screen", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for settings overlay hidden", () => settingsOverlay().Expanded.Value, () => Is.False);
PlayerSettingsOverlay settingsOverlay() => Player.ChildrenOfType<PlayerSettingsOverlay>().Single();
ReplaySettingsOverlay settingsOverlay() => Player.ChildrenOfType<ReplaySettingsOverlay>().Single();
}
private void loadPlayerWithBeatmap(IBeatmap? beatmap = null)
@@ -503,27 +503,27 @@ namespace osu.Game.Tests.Visual.Gameplay
InputManager.Click(MouseButton.Left);
});
AddStep("Right-click TopLeft anchor", () =>
AddStep("Click TopLeft anchor", () =>
{
InputManager.MoveMouseTo(getMenuItemByText("TopLeft"));
InputManager.Click(MouseButton.Right);
InputManager.Click(MouseButton.Left);
});
AddAssert("TopLeft item checked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
AddStep("Right-click Centre anchor", () =>
AddStep("Click Centre anchor", () =>
{
InputManager.MoveMouseTo(getMenuItemByText("Centre"));
InputManager.Click(MouseButton.Right);
InputManager.Click(MouseButton.Left);
});
AddAssert("Centre item checked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
AddAssert("TopLeft item unchecked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False);
AddStep("Right-click Closest anchor", () =>
AddStep("Click Closest anchor", () =>
{
InputManager.MoveMouseTo(getMenuItemByText("Closest"));
InputManager.Click(MouseButton.Right);
InputManager.Click(MouseButton.Left);
});
AddAssert("Closest item checked", () => (getMenuItemByText("Closest").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
@@ -331,7 +331,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
p.RequestResults = _ => resultsRequested = true;
});
AddUntilStep("wait for load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any() && playlist.ChildrenOfType<BeatmapCardThumbnail>().First().DrawWidth > 0);
AddUntilStep("wait for load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any()
&& playlist.ChildrenOfType<LinkFlowContainer>().First().ChildrenOfType<SpriteText>().Any()
&& playlist.ChildrenOfType<BeatmapCardThumbnail>().First().DrawWidth > 0);
AddStep("move mouse to first item title", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<LinkFlowContainer>().First().ChildrenOfType<SpriteText>().First()));
AddAssert("first item title not hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.False);
@@ -50,47 +50,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.CacheAs(ongoingOperationTracker = new OngoingOperationTracker());
Dependencies.CacheAs(availabilityTracker.Object);
availabilityTracker.SetupGet(a => a.Availability).Returns(beatmapAvailability);
multiplayerClient.SetupGet(m => m.LocalUser).Returns(() => localUser);
multiplayerClient.SetupGet(m => m.Room).Returns(() => multiplayerRoom);
// By default, the local user is to be the host.
multiplayerClient.SetupGet(m => m.IsHost).Returns(() => ReferenceEquals(multiplayerRoom.Host, localUser));
// Assume all state changes are accepted by the server.
multiplayerClient.Setup(m => m.ChangeState(It.IsAny<MultiplayerUserState>()))
.Callback((MultiplayerUserState r) =>
{
Logger.Log($"Changing local user state from {localUser.State} to {r}");
localUser.State = r;
raiseRoomUpdated();
});
multiplayerClient.Setup(m => m.StartMatch())
.Callback(() =>
{
multiplayerClient.Raise(m => m.LoadRequested -= null);
// immediately "end" gameplay, as we don't care about that part of the process.
changeUserState(localUser.UserID, MultiplayerUserState.Idle);
});
multiplayerClient.Setup(m => m.SendMatchRequest(It.IsAny<MatchUserRequest>()))
.Callback((MatchUserRequest request) =>
{
switch (request)
{
case StartMatchCountdownRequest countdownStart:
setRoomCountdown(countdownStart.Duration);
break;
case StopCountdownRequest:
clearRoomCountdown();
break;
}
});
Children = new Drawable[]
{
ongoingOperationTracker,
@@ -103,10 +62,51 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("reset state", () =>
{
multiplayerClient.Invocations.Clear();
multiplayerClient.Reset();
multiplayerClient.SetupGet(m => m.LocalUser).Returns(() => localUser);
multiplayerClient.SetupGet(m => m.Room).Returns(() => multiplayerRoom);
// By default, the local user is to be the host.
multiplayerClient.SetupGet(m => m.IsHost).Returns(() => ReferenceEquals(multiplayerRoom.Host, localUser));
// Assume all state changes are accepted by the server.
multiplayerClient.Setup(m => m.ChangeState(It.IsAny<MultiplayerUserState>()))
.Callback((MultiplayerUserState r) =>
{
Logger.Log($"Changing local user state from {localUser.State} to {r}");
localUser.State = r;
raiseRoomUpdated();
});
multiplayerClient.Setup(m => m.StartMatch())
.Callback(() =>
{
multiplayerClient.Raise(m => m.LoadRequested -= null);
// immediately "end" gameplay, as we don't care about that part of the process.
changeUserState(localUser.UserID, MultiplayerUserState.Idle);
});
multiplayerClient.Setup(m => m.SendMatchRequest(It.IsAny<MatchUserRequest>()))
.Callback((MatchUserRequest request) =>
{
switch (request)
{
case StartMatchCountdownRequest countdownStart:
setRoomCountdown(countdownStart.Duration);
break;
case StopCountdownRequest:
clearRoomCountdown();
break;
}
});
beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable();
availabilityTracker.Reset();
availabilityTracker.SetupGet(a => a.Availability).Returns(beatmapAvailability);
PlaylistItem item = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
@@ -375,6 +375,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestAbortMatch()
{
setUpMatchCallbacks();
// Ready
ClickButtonWhenEnabled<MultiplayerReadyButton>();
// Start match
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
// Abort
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
}
private void setUpMatchCallbacks()
{
AddStep("setup client", () =>
{
@@ -383,6 +399,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
multiplayerClient.Raise(m => m.LoadRequested -= null);
multiplayerClient.Object.Room!.State = MultiplayerRoomState.WaitingForLoad;
raiseRoomUpdated();
// The local user state doesn't really matter, so let's do the same as the base implementation for these tests.
changeUserState(localUser.UserID, MultiplayerUserState.Idle);
@@ -395,19 +412,133 @@ namespace osu.Game.Tests.Visual.Multiplayer
raiseRoomUpdated();
});
});
}
// Ready
ClickButtonWhenEnabled<MultiplayerReadyButton>();
[Test]
public void TestRefereeSpectating()
{
AddStep("set up referee", () =>
{
multiplayerClient.SetupGet(m => m.IsReferee).Returns(true);
multiplayerClient.SetupGet(m => m.IsHost).Returns(false);
multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee;
raiseRoomUpdated();
});
// Start match
const int users = 10;
AddStep("add many users", () =>
{
for (int i = 0; i < users; i++)
addUser(new APIUser { Id = i, Username = "Another user" });
});
AddAssert("button disabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.False);
AddStep("move to spectate", () => changeUserState(multiplayerClient.Object.LocalUser!.UserID, MultiplayerUserState.Spectating));
AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready));
AddAssert("button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.True);
setUpMatchCallbacks();
// start match
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
// Abort
// abort
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
}
[Test]
public void TestRefereeFlowWithoutCountdown()
{
AddStep("set up referee", () =>
{
multiplayerClient.SetupGet(m => m.IsReferee).Returns(true);
multiplayerClient.SetupGet(m => m.IsHost).Returns(false);
multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee;
raiseRoomUpdated();
});
const int users = 10;
AddStep("add many users", () =>
{
for (int i = 0; i < users; i++)
addUser(new APIUser { Id = i, Username = "Another user" });
});
AddAssert("button disabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.False);
AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready));
AddAssert("button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.True);
setUpMatchCallbacks();
// start match
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
// abort
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
}
[Test]
public void TestRefereeFlowWithCountdown()
{
AddStep("set up referee", () =>
{
multiplayerClient.SetupGet(m => m.IsReferee).Returns(true);
multiplayerClient.SetupGet(m => m.IsHost).Returns(false);
multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee;
raiseRoomUpdated();
});
const int users = 10;
AddStep("add many users", () =>
{
for (int i = 0; i < users; i++)
addUser(new APIUser { Id = i, Username = "Another user" });
});
AddAssert("button disabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.False);
AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready));
AddAssert("button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.True);
setUpMatchCallbacks();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
AddStep("check request received", () =>
{
multiplayerClient.Verify(m => m.SendMatchRequest(It.Is<StartMatchCountdownRequest>(req =>
req.Duration == TimeSpan.FromSeconds(10)
)), Times.Once);
});
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the cancel button", () =>
{
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().Last();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
AddStep("check request received", () =>
{
multiplayerClient.Verify(m => m.SendMatchRequest(It.IsAny<StopCountdownRequest>()), Times.Once);
});
}
private void verifyGameplayStartFlow()
{
checkLocalUserState(MultiplayerUserState.Ready);
@@ -156,7 +156,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
// components wrapped in skinnable target containers load asynchronously, potentially taking more than one frame to load.
// therefore use until step rather than direct assert to account for that.
AddUntilStep("all interactive elements removed", () => this.ChildrenOfType<Player>().All(p =>
!p.ChildrenOfType<PlayerSettingsOverlay>().Any() &&
!p.ChildrenOfType<ReplaySettingsOverlay>().Any() &&
!p.ChildrenOfType<HoldForMenuButton>().Any() &&
p.ChildrenOfType<ArgonSongProgressBar>().SingleOrDefault()?.Interactive == false));
@@ -662,7 +662,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("invoke on back button", () => multiplayerComponents.OnBackButton());
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().State.Value == Visibility.Hidden);
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().All(o => o.State.Value == Visibility.Hidden));
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
@@ -17,6 +17,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
@@ -57,6 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
Dependencies.CacheAs<BeatmapStore>(new RealmDetachedBeatmapStore());
Dependencies.Cache(new ChannelManager(API));
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
@@ -351,6 +353,30 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("button hidden", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.EqualTo(0));
}
[Test]
public void TestChangeSettingsButtonAlwaysVisibleForReferee()
{
AddStep("add playlist item", () =>
{
room.Playlist =
[
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}
];
});
AddStep("setup referee", () => MultiplayerClient.RoomSetupAction = r => r.Host!.Role = MultiplayerRoomUserRole.Referee);
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
AddUntilStep("wait for join", () => RoomJoined);
AddUntilStep("button visible", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.GreaterThan(0));
AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }));
AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID));
AddAssert("button hidden", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.GreaterThan(0));
}
[Test]
public void TestUserModSelectUpdatesWhenNotVisible()
{
@@ -439,6 +465,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("countdown started", () => MultiplayerClient.ServerRoom!.ActiveCountdowns.Any());
}
[Test]
public void TestRoll()
{
AddStep("set playlist", () =>
{
room.Playlist =
[
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}
];
});
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
AddStep("set channel", () => room.ChannelId = 1);
AddUntilStep("wait for room join", () => RoomJoined);
AddStep("roll", () => MultiplayerClient.SendMatchRequest(new RollRequest()));
}
[Test]
public void TestSettingsRemainsOpenOnRoomUpdate()
{
@@ -32,10 +32,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiplayerParticipantsList : MultiplayerTestScene
{
public override void SetUpSteps()
private void setUpList()
{
base.SetUpSteps();
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
WaitForJoined();
createNewParticipantsList();
@@ -44,6 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestAddUser()
{
setUpList();
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
@@ -56,9 +55,30 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 2);
}
[Test]
public void TestAddReferee()
{
setUpList();
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
AddStep("add user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(3)
{
User = new APIUser
{
Id = 3,
Username = "Second",
CoverUrl = TestResources.COVER_IMAGE_3,
},
Role = MultiplayerRoomUserRole.Referee
}));
AddAssert("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 2);
}
[Test]
public void TestAddUnresolvedUser()
{
setUpList();
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
AddStep("add non-resolvable user", () => MultiplayerClient.TestAddUnresolvedUser());
@@ -75,6 +95,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestRemoveUser()
{
setUpList();
APIUser? secondUser = null;
AddStep("add a user", () =>
@@ -95,6 +117,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestGameStateHasPriorityOverDownloadState()
{
setUpList();
AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
checkProgressBarVisibility(true);
@@ -109,6 +132,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestCorrectInitialState()
{
setUpList();
AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
createNewParticipantsList();
checkProgressBarVisibility(true);
@@ -117,6 +141,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestBeatmapDownloadingStates()
{
setUpList();
AddStep("set to unknown", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Unknown()));
AddStep("set to no map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()));
AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
@@ -140,6 +165,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestToggleReadyState()
{
setUpList();
AddAssert("ready mark invisible", () => !this.ChildrenOfType<StateDisplay>().Single().IsPresent);
AddStep("make user ready", () => MultiplayerClient.ChangeState(MultiplayerUserState.Ready));
@@ -152,6 +178,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestToggleSpectateState()
{
setUpList();
AddStep("make user spectating", () => MultiplayerClient.ChangeState(MultiplayerUserState.Spectating));
AddStep("make user idle", () => MultiplayerClient.ChangeState(MultiplayerUserState.Idle));
}
@@ -159,6 +186,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestCrownChangesStateWhenHostTransferred()
{
setUpList();
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
{
Id = 3,
@@ -182,6 +210,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestHostGetsPinnedToTop()
{
setUpList();
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
{
Id = 3,
@@ -199,8 +228,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
public void TestKickButtonOnlyPresentWhenHost()
public void TestKickButtonPresentWhenHost()
{
setUpList();
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
{
Id = 3,
@@ -219,9 +249,33 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
}
[Test]
public void TestKickButtonPresentWhenReferee()
{
AddStep("set up referee", () => MultiplayerClient.RoomSetupAction = r => r.Host!.Role = MultiplayerRoomUserRole.Referee);
setUpList();
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
{
Id = 3,
Username = "Second",
CoverUrl = TestResources.COVER_IMAGE_3,
}));
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
AddStep("make second user host", () => MultiplayerClient.TransferHost(3));
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
AddStep("make local user host again", () => MultiplayerClient.TransferHost(API.LocalUser.Value.Id));
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
}
[Test]
public void TestKickButtonKicks()
{
setUpList();
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
{
Id = 3,
@@ -239,6 +293,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
const int users_count = 200;
setUpList();
AddStep("add many users", () =>
{
for (int i = 0; i < users_count; i++)
@@ -297,6 +352,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestUserWithMods()
{
setUpList();
AddStep("add user", () =>
{
MultiplayerClient.AddUser(new APIUser
@@ -334,6 +390,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestUserWithStyle()
{
setUpList();
AddStep("add users", () =>
{
MultiplayerClient.AddUser(new APIUser
@@ -361,6 +418,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestModOverlap()
{
setUpList();
AddStep("add dummy mods", () =>
{
MultiplayerClient.ChangeUserMods(new Mod[]
@@ -419,6 +477,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestModsAndRuleset()
{
setUpList();
AddStep("add another user", () =>
{
MultiplayerClient.AddUser(new APIUser
@@ -453,6 +512,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestTeams()
{
setUpList();
AddStep("enable teams", () => MultiplayerClient.ChangeSettings(matchType: MatchType.TeamVersus));
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
@@ -41,10 +41,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.Cache(Realm);
}
public override void SetUpSteps()
private void setUpRoom()
{
base.SetUpSteps();
AddStep("create room", () => room = CreateDefaultRoom());
AddStep("join room", () => JoinRoom(room));
WaitForJoined();
@@ -80,6 +78,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestDeleteButtonAlwaysVisibleForHost()
{
setUpRoom();
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
@@ -92,6 +92,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestDeleteButtonOnlyVisibleForItemOwnerIfNotHost()
{
setUpRoom();
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
@@ -108,9 +110,35 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertDeleteButtonVisibility(2, true);
}
[Test]
public void TestDeleteButtonAlwaysVisibleForReferee()
{
AddStep("ensure host will be referee", () => MultiplayerClient.RoomSetupAction = r => r.Host!.Role = MultiplayerRoomUserRole.Referee);
setUpRoom();
AddStep("join other user", () => MultiplayerClient.AddUser(new APIUser { Id = 1234 }));
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
addPlaylistItem(() => API.LocalUser.Value.OnlineID);
assertDeleteButtonVisibility(1, true);
addPlaylistItem(() => 1234);
assertDeleteButtonVisibility(2, true);
AddStep("set host only queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.HostOnly }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.HostOnly);
AddStep("set other user as host", () => MultiplayerClient.TransferHost(1234));
assertDeleteButtonVisibility(1, true);
assertDeleteButtonVisibility(2, true);
}
[Test]
public void TestSingleItemDoesNotHaveDeleteButton()
{
setUpRoom();
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
@@ -120,6 +148,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestCurrentItemHasDeleteButtonIfNotSingle()
{
setUpRoom();
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
@@ -139,6 +169,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestChangeExistingItem()
{
setUpRoom();
AddStep("change beatmap", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem
{
ID = playlist.Items[0].ID,
@@ -116,6 +116,55 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("user still on team 0", () => (multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
}
[Test]
public void TestTeamChangesLocked()
{
createRoom(() => new Room
{
Name = "Test Room",
Type = MatchType.TeamVersus,
Playlist =
[
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}
]
});
AddUntilStep("user on team 0", () => (multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
AddStep("add another user", () => multiplayerClient.AddUser(new APIUser { Username = "otheruser", Id = 44 }));
AddStep("lock room", () =>
{
var roomState = TeamVersusRoomState.CreateDefault();
roomState.Locked = true;
multiplayerClient.ChangeMatchRoomState(roomState).WaitSafely();
});
AddStep("press own button", () =>
{
InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType<TeamDisplay>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("user on team 0", () => (multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
AddStep("unlock room", () =>
{
var roomState = TeamVersusRoomState.CreateDefault();
roomState.Locked = false;
multiplayerClient.ChangeMatchRoomState(roomState).WaitSafely();
});
AddStep("press own button", () =>
{
InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType<TeamDisplay>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("user on team 1", () => (multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1);
AddStep("press own button again", () => InputManager.Click(MouseButton.Left));
AddUntilStep("user on team 0", () => (multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
}
[Test]
public void TestSettingsUpdatedWhenChangingMatchType()
{
@@ -653,6 +653,7 @@ namespace osu.Game.Tests.Visual.Navigation
}
[Test]
[FlakyTest]
public void TestDeleteScoreAfterPlaying()
{
playToResults();
@@ -300,7 +300,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("move cursor to right of screen too far", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(10240, 0)));
AddUntilStep("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0));
PlayerSettingsOverlay getPlayerSettingsOverlay() => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PlayerSettingsOverlay>().SingleOrDefault();
ReplaySettingsOverlay getPlayerSettingsOverlay() => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<ReplaySettingsOverlay>().SingleOrDefault();
}
[Test]
@@ -12,7 +12,6 @@ using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Metadata;
@@ -219,13 +218,13 @@ namespace osu.Game.Tests.Visual.Online
}
private void waitForLoad()
=> AddUntilStep("wait for panels to load", () => this.ChildrenOfType<LoadingSpinner>().First().State.Value, () => Is.EqualTo(Visibility.Hidden));
=> AddUntilStep("wait for panels to load", () => this.ChildrenOfType<UserPanel>().Any());
private void assertVisiblePanelCount<T>(int expectedVisible)
where T : UserPanel
{
AddAssert($"{typeof(T).ReadableName()}s in list", () => this.ChildrenOfType<FriendsList>().Last().ChildrenOfType<UserPanel>().All(p => p is T));
AddAssert($"{expectedVisible} panels visible", () => this.ChildrenOfType<FriendsList>().Last().ChildrenOfType<FriendsList.FilterableUserPanel>().Count(p => p.IsPresent),
AddUntilStep($"{typeof(T).ReadableName()}s in list", () => this.ChildrenOfType<FriendsList>().Last().ChildrenOfType<UserPanel>().All(p => p is T));
AddUntilStep($"{expectedVisible} panels visible", () => this.ChildrenOfType<FriendsList>().Last().ChildrenOfType<FriendsList.FilterableUserPanel>().Count(p => p.IsPresent),
() => Is.EqualTo(expectedVisible));
}
@@ -0,0 +1,24 @@
// 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.Allocation;
using osu.Game.Overlays;
using osu.Game.Overlays.Dashboard.UserSearch;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneUserSearchDisplay : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
protected override bool UseOnlineAPI => true;
[SetUp]
public void Setup() => Schedule(() =>
{
Child = new UserSearchDisplay();
});
}
}
@@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers.Markdown;
using osu.Game.Graphics.Containers.Markdown.Footnotes;
using osu.Game.Overlays;
@@ -143,98 +142,11 @@ outdated: true # not sure about the format for ""list of mods"".
AddAssert("No notice box visible", () => !markdownContainer.ChildrenOfType<Container>().Any());
}
[Test]
public void TestAbsoluteImage()
{
AddStep("Add absolute image", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh";
markdownContainer.Text = "![intro](/wiki/images/Client/Interface/img/intro-screen.jpg)";
});
}
[Test]
public void TestRelativeImage()
{
AddStep("Add relative image", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
markdownContainer.Text = "![intro](../images/Client/Interface/img/intro-screen.jpg)";
});
}
[Test]
public void TestBlockImage()
{
AddStep("Add paragraph with block image", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
markdownContainer.Text = @"Line before image
![play menu](../images/Client/Interface/img/play-menu.jpg ""Main Menu in osu!"")
Line after image";
});
}
[Test]
public void TestInlineImage()
{
AddStep("Add inline image", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh";
markdownContainer.Text = "![osu! mode icon](/wiki/shared/mode/osu.png) osu!";
});
}
[Test]
public void TestTableWithImageContent()
{
AddStep("Add Table", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh";
markdownContainer.Text = @"
| Image | Name | Effect |
| :-: | :-: | :-- |
| ![](/wiki/images/shared/judgement/osu!/hit300.png ""300"") | 300 | A possible score when tapping a hit circle precisely on time, completing a Slider and keeping the cursor over every tick, or completing a Spinner with the Spinner Metre full. A score of 300 appears in an blue score by default. Scoring nothing except 300s in a beatmap will award the player with the SS or SSH grade. |
| ![](/wiki/images/shared/judgement/osu!/hit300g.png ""Geki"") | () Geki | A term from Ouendan, called Elite Beat! in EBA. Appears when playing the last element in a combo in which the player has scored only 300s. Getting a Geki will give a sizable boost to the Life Bar. By default, it is blue. |
| ![](/wiki/images/shared/judgement/osu!/hit100.png ""100"") | 100 | A possible score one can get when tapping a Hit Object slightly late or early, completing a Slider and missing a number of ticks, or completing a Spinner with the Spinner Meter almost full. A score of 100 appears in a green score by default. When very skilled players test a beatmap and they get a lot of 100s, this may mean that the beatmap does not have correct timing. |
| ![](/wiki/images/shared/judgement/osu!/hit300k.png ""300 Katu"") ![](/wiki/Skinning/Interface/img/hit100k.png ""100 Katu"") | () Katu or Katsu | A term from Ouendan, called Beat! in EBA. Appears when playing the last element in a combo in which the player has scored at least one 100, but no 50s or misses. Getting a Katu will give a small boost to the Life Bar. By default, it is coloured green or blue depending on whether the Katu itself is a 100 or a 300. |
| ![](/wiki/images/shared/judgement/osu!/hit50.png ""50"") | 50 | A possible score one can get when tapping a hit circle rather early or late but not early or late enough to cause a miss, completing a Slider and missing a lot of ticks, or completing a Spinner with the Spinner Metre close to full. A score of 50 appears in a orange score by default. Scoring a 50 in a combo will prevent the appearance of a Katu or a Geki at the combo's end. |
| ![](/wiki/images/shared/judgement/osu!/hit0.png ""Miss"") | Miss | A possible score one can get when not tapping a hit circle or too early (based on OD and AR, it may *shake* instead), not tapping or holding the Slider at least once, or completing a Spinner with low Spinner Metre fill. Scoring a Miss will reset the current combo to 0 and will prevent the appearance of a Katu or a Geki at the combo's end. |
";
});
}
[Test]
public void TestWideImageNotExceedContainer()
{
AddStep("Add image", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/osu!_Program_Files/";
markdownContainer.Text = "![](../images/Client/Program_files/img/file_structure.jpg \"The file structure of osu!'s installation folder, on Windows and macOS\")";
});
AddUntilStep("Wait image to load", () => markdownContainer.ChildrenOfType<DelayedLoadWrapper>().First().DelayedLoadCompleted);
AddStep("Change container width", () =>
{
markdownContainer.Width = 0.5f;
});
AddAssert("Image not exceed container width", () =>
{
var spriteImage = markdownContainer.ChildrenOfType<Sprite>().First();
return Precision.DefinitelyBigger(markdownContainer.DrawWidth, spriteImage.DrawWidth);
});
}
[Test]
public void TestFlag()
{
AddStep("Add flag", () =>
{
markdownContainer.CurrentPath = @"https://dev.ppy.sh";
markdownContainer.Text = "::{flag=\"AU\"}:: ::{flag=\"ZZ\"}::";
});
AddAssert("Two flags visible", () => markdownContainer.ChildrenOfType<DrawableFlag>().Count(), () => Is.EqualTo(2));
@@ -140,13 +140,14 @@ namespace osu.Game.Tests.Visual.Playlists
room = new Room
{
RoomID = 1,
MaxAttempts = 10,
Playlist =
[
// osu! beatmap
new PlaylistItem(importedSet.Beatmaps[0])
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
Freestyle = true
Freestyle = true,
},
// osu! beatmap converted played in taiko
new PlaylistItem(importedSet.Beatmaps[1])
@@ -0,0 +1,52 @@
// 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.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components;
namespace osu.Game.Tests.Visual.RankedPlay
{
public partial class TestSceneBubbleChatHistory : OsuTestScene
{
private RankedPlayChatDisplay.BubbleChatHistory history = null!;
[SetUp]
public void Setup() => Schedule(() =>
{
Child = history = new RankedPlayChatDisplay.BubbleChatHistory
{
Anchor = Anchor.Centre,
Origin = Anchor.BottomCentre,
Width = 300
};
});
[Test]
public void TestPostMessages()
{
int messageId = 1;
AddRepeatStep("post message", () => history.PostMessage(new APIUser { Id = 2 }, $"message {messageId++}"), 20);
}
[Test]
public void TestCollapse()
{
AddStep("set expanded", () => history.Expand());
AddStep("post some messages", () =>
{
for (int i = 0; i < 10; i++)
history.PostMessage(new APIUser { Id = 2 }, $"message {i}");
});
AddWaitStep("wait a bit", 10);
AddStep("set collapsed", () => history.Collapse());
AddWaitStep("wait a bit", 10);
AddStep("set expanded", () => history.Expand());
AddWaitStep("wait a bit", 10);
AddStep("set collapsed", () => history.Collapse());
}
}
}
@@ -2,15 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.Visual.RankedPlay
{
public partial class TestSceneDiscardScreen : MultiplayerTestScene
public partial class TestSceneDiscardScreen : RankedPlayTestScene
{
private RankedPlayScreen screen = null!;
@@ -26,7 +26,26 @@ namespace osu.Game.Tests.Visual.RankedPlay
AddStep("load screen", () => LoadScreen(screen = new RankedPlayScreen(MultiplayerClient.ClientRoom!)));
AddUntilStep("screen loaded", () => screen.IsLoaded);
var requestHandler = new BeatmapRequestHandler();
AddStep("setup request handler", () => ((DummyAPIAccess)API).HandleRequest = requestHandler.HandleRequest);
AddStep("set pick state", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.CardDiscard).WaitSafely());
AddWaitStep("wait some", 5);
AddStep("reveal cards", () =>
{
for (int i = 0; i < 5; i++)
{
int i2 = i;
MultiplayerClient.RankedPlayRevealCard(hand => hand[i2], new MultiplayerPlaylistItem
{
ID = i2,
BeatmapID = requestHandler.Beatmaps[i2].OnlineID
}).WaitSafely();
}
});
}
}
}
@@ -75,14 +75,14 @@ namespace osu.Game.Tests.Visual.RankedPlay
}
private double flushInterval = 1000;
private double recordInterval = 25;
private double recordInterval = 50;
private double fixedLatency;
private double maxLatency;
[Test]
public void TestCardHandReplay()
{
AddSliderStep("record interval", 0.0, 1000.0, 25.0, value =>
AddSliderStep("record interval", 0.0, 1000.0, 50.0, value =>
{
recordInterval = value;
recreateRecorder();
@@ -2,15 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.Visual.RankedPlay
{
public partial class TestSceneOpponentPickScreen : MultiplayerTestScene
public partial class TestSceneOpponentPickScreen : RankedPlayTestScene
{
private RankedPlayScreen screen = null!;
@@ -26,7 +26,33 @@ namespace osu.Game.Tests.Visual.RankedPlay
AddStep("load screen", () => LoadScreen(screen = new RankedPlayScreen(MultiplayerClient.ClientRoom!)));
AddUntilStep("screen loaded", () => screen.IsLoaded);
var requestHandler = new BeatmapRequestHandler();
AddStep("setup request handler", () => ((DummyAPIAccess)API).HandleRequest = requestHandler.HandleRequest);
AddStep("set pick state", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.CardPlay, state => state.ActiveUserId = 2).WaitSafely());
AddStep("reveal cards", () =>
{
for (int i = 0; i < 5; i++)
{
int i2 = i;
MultiplayerClient.RankedPlayRevealCard(hand => hand[i2], new MultiplayerPlaylistItem
{
ID = i2,
BeatmapID = requestHandler.Beatmaps[i2].OnlineID
}).WaitSafely();
}
});
AddWaitStep("wait", 15);
AddStep("play beatmap", () => MultiplayerClient.PlayUserCard(2, hand => hand[0]).WaitSafely());
AddStep("reveal card", () => MultiplayerClient.RankedPlayRevealUserCard(2, hand => hand[0], new MultiplayerPlaylistItem
{
ID = 0,
BeatmapID = requestHandler.Beatmaps[0].OnlineID
}).WaitSafely());
}
}
}
@@ -1,16 +1,21 @@
// 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 osu.Framework.Extensions;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand;
using osuTK.Input;
namespace osu.Game.Tests.Visual.RankedPlay
{
public partial class TestScenePickScreen : MultiplayerTestScene
public partial class TestScenePickScreen : RankedPlayTestScene
{
private RankedPlayScreen screen = null!;
@@ -26,7 +31,50 @@ namespace osu.Game.Tests.Visual.RankedPlay
AddStep("load screen", () => LoadScreen(screen = new RankedPlayScreen(MultiplayerClient.ClientRoom!)));
AddUntilStep("screen loaded", () => screen.IsLoaded);
var requestHandler = new BeatmapRequestHandler();
AddStep("setup request handler", () => ((DummyAPIAccess)API).HandleRequest = requestHandler.HandleRequest);
AddStep("set pick state", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.CardPlay, state => state.ActiveUserId = API.LocalUser.Value.OnlineID).WaitSafely());
AddWaitStep("wait some", 5);
AddStep("reveal cards", () =>
{
for (int i = 0; i < 5; i++)
{
int i2 = i;
MultiplayerClient.RankedPlayRevealCard(hand => hand[i2], new MultiplayerPlaylistItem
{
ID = i2,
BeatmapID = requestHandler.Beatmaps[i2].OnlineID
}).WaitSafely();
}
});
for (int i = 0; i < 3; i++)
{
int i2 = i;
AddStep($"click card {i2}", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PlayerHandOfCards.PlayerHandCard>().ElementAt(i2));
InputManager.Click(MouseButton.Left);
});
}
AddWaitStep("wait", 3);
AddStep("click play button", () =>
{
var button = screen
.ChildrenOfType<PlayerHandOfCards.PlayerHandCard>()
.First(it => it.Selected)
.ChildrenOfType<ShearedButton>()
.First();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
}
}
}
@@ -6,6 +6,7 @@ using Humanizer;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
@@ -33,12 +34,23 @@ namespace osu.Game.Tests.Visual.RankedPlay
};
}
[SetUpSteps]
public void SetupSteps()
{
AddStep("reset card hand", () => Child = handOfCards = new PlayerHandOfCards
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
});
}
[Test]
public void TestSingleSelectionMode()
{
AddStep("add cards", () =>
{
handOfCards.Clear();
for (int i = 0; i < 5; i++)
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
});
@@ -59,7 +71,6 @@ namespace osu.Game.Tests.Visual.RankedPlay
{
AddStep("add cards", () =>
{
handOfCards.Clear();
for (int i = 0; i < 5; i++)
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
});
@@ -84,7 +95,13 @@ namespace osu.Game.Tests.Visual.RankedPlay
AddStep($"{i} {"cards".Pluralize(i == 1)}", () =>
{
handOfCards.Clear();
Child = handOfCards = new PlayerHandOfCards
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
};
for (int j = 0; j < numCards; j++)
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
@@ -138,7 +155,6 @@ namespace osu.Game.Tests.Visual.RankedPlay
{
AddStep("add cards", () =>
{
handOfCards.Clear();
for (int i = 0; i < 5; i++)
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
});
@@ -157,5 +173,24 @@ namespace osu.Game.Tests.Visual.RankedPlay
AddAssert("card selected", () => handOfCards.Selection.Contains(handOfCards.Cards.ElementAt(i1).Card.Item));
}
}
[Test]
public void TestContract()
{
AddStep("add cards", () =>
{
for (int i = 0; i < 5; i++)
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
});
AddWaitStep("wait", 5);
AddStep("contract", () => handOfCards.Contract());
AddWaitStep("wait", 5);
AddAssert(
"all cards outside bounds", () =>
handOfCards
.ChildrenOfType<HandOfCards.HandCard>()
.All(card => !card.ScreenSpaceDrawQuad.AABBFloat.IntersectsWith(handOfCards.ScreenSpaceDrawQuad.AABBFloat))
);
}
}
}
@@ -17,7 +17,6 @@ namespace osu.Game.Tests.Visual.RankedPlay
private readonly Bindable<Colour4> gradientOuter = new Bindable<Colour4>(Color4Extensions.FromHex("AC6D97"));
private readonly Bindable<Colour4> gradientInner = new Bindable<Colour4>(Color4Extensions.FromHex("544483"));
private readonly Bindable<Colour4> dots = new Bindable<Colour4>(Color4Extensions.FromHex("D56CF6"));
public TestSceneRankedPlayBackground()
{
@@ -40,11 +39,6 @@ namespace osu.Game.Tests.Visual.RankedPlay
Scale = new Vector2(0.4f),
Current = gradientInner,
},
new BasicColourPicker
{
Scale = new Vector2(0.4f),
Current = dots,
}
]
}
];
@@ -54,9 +48,8 @@ namespace osu.Game.Tests.Visual.RankedPlay
{
base.LoadComplete();
gradientOuter.BindValueChanged(e => background.GradientOutside = e.NewValue, true);
gradientInner.BindValueChanged(e => background.GradientInside = e.NewValue, true);
dots.BindValueChanged(e => background.DotsColour = e.NewValue, true);
gradientOuter.BindValueChanged(e => background.GradientBottom = e.NewValue, true);
gradientInner.BindValueChanged(e => background.GradientTop = e.NewValue, true);
}
}
}
@@ -0,0 +1,129 @@
// 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.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.Visual.RankedPlay
{
public partial class TestSceneRankedPlayChat : MultiplayerTestScene
{
private ChannelManager channelManager = null!;
private Channel testChannel = null!;
private int messageIdSequence;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var api = parent.Get<IAPIProvider>();
Add(channelManager = new ChannelManager(api));
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(channelManager);
return dependencies;
}
[SetUp]
public void SetUp() => Schedule(() =>
{
messageIdSequence = 0;
testChannel = channelManager.JoinChannel(new Channel { Id = 1, Type = ChannelType.Multiplayer });
});
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("join room", () =>
{
var room = CreateDefaultRoom(MatchType.RankedPlay);
room.ChannelId = 1;
JoinRoom(room);
});
WaitForJoined();
AddStep("join other user", () => MultiplayerClient.AddUser(new APIUser { Id = 2 }));
AddStep("load screen", () => LoadScreen(new RankedPlayScreen(MultiplayerClient.ClientRoom!)));
}
[Test]
public void TestDiscardCardStage()
{
AddStep("set discard phase", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.CardDiscard).WaitSafely());
postLocalUserMessage("this is a message from the local user");
postOpponentMessage("this is a message from the opponent");
}
[Test]
public void TestResultsStage()
{
AddStep("set results state", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.Results, state =>
{
int losingPlayer = state.Users.Keys.First();
foreach (var (id, userInfo) in state.Users)
{
if (id == losingPlayer)
{
userInfo.DamageInfo = new RankedPlayDamageInfo
{
RawDamage = 123_456,
Damage = 123_456,
OldLife = 500_000,
NewLife = 500_000 - 123_456,
};
userInfo.Life = 500_000 - 123_456;
}
else
{
userInfo.DamageInfo = new RankedPlayDamageInfo
{
RawDamage = 0,
Damage = 0,
OldLife = 1_000_000,
NewLife = 1_000_000,
};
}
}
}).WaitSafely());
}
private void postLocalUserMessage(string content)
{
AddStep("add local user message", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Timestamp = DateTimeOffset.Now,
Sender = API.LocalUser.Value,
Content = content
}));
}
private void postOpponentMessage(string content)
{
AddStep("add opponent message", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Timestamp = DateTimeOffset.Now,
Sender = new APIUser
{
Id = 2,
Username = "peppy"
},
Content = content
}));
}
}
}
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components;
using osu.Game.Tests.Visual.Multiplayer;
@@ -27,7 +28,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
new RankedPlayCornerPiece(RankedPlayColourScheme.Blue, Anchor.BottomLeft)
{
State = { BindTarget = visibility },
Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
Child = new RankedPlayUserDisplay(new APIUser { Id = 2, Username = "peppy" }, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
{
RelativeSizeAxes = Axes.Both,
}
@@ -35,7 +36,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
new RankedPlayCornerPiece(RankedPlayColourScheme.Red, Anchor.TopRight)
{
State = { BindTarget = visibility },
Child = new RankedPlayUserDisplay(2, Anchor.TopRight, RankedPlayColourScheme.Red)
Child = new RankedPlayUserDisplay(new APIUser { Id = 2, Username = "peppy" }, Anchor.TopRight, RankedPlayColourScheme.Red)
{
RelativeSizeAxes = Axes.Both,
}
@@ -0,0 +1,91 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
namespace osu.Game.Tests.Visual.RankedPlay
{
public partial class TestSceneRankedPlayStageOverlay : RankedPlayTestScene
{
private Container content = null!;
protected override Container<Drawable> Content => content;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("create components", () => base.Content.Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new RankedPlayBackground
{
RelativeSizeAxes = Axes.Both,
},
content = new Container
{
RelativeSizeAxes = Axes.Both,
},
}
});
}
[Test]
public void TestBasic()
{
AddStep("create", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Blue)
{
PickingUser = new APIUser
{
Id = 2,
Username = "peppy",
},
Multiplier = 2,
});
}
[Test]
public void TestLongUsername()
{
AddStep("create", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Blue)
{
PickingUser = new APIUser
{
Id = 226597,
Username = "WWWWWWWWWWWWWWWWWWWW",
},
Multiplier = 2,
});
}
[Test]
public void TestColourScheme()
{
AddStep("create blue", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Blue)
{
PickingUser = new APIUser
{
Id = 2,
Username = "peppy",
},
Multiplier = 2,
});
AddStep("create red", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Red)
{
PickingUser = new APIUser
{
Id = 2,
Username = "peppy",
},
Multiplier = 2,
});
}
}
}
@@ -4,6 +4,9 @@
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Online.Rooms;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components;
using osu.Game.Tests.Visual.Multiplayer;
@@ -20,11 +23,19 @@ namespace osu.Game.Tests.Visual.RankedPlay
Value = 1_000_000,
};
public TestSceneRankedPlayUserDisplay()
{
AddSliderStep("health", 0, 1_000_000, 1_000_000, value => health.Value = value);
}
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("add display", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.RankedPlay)));
WaitForJoined();
AddStep("add display", () => Child = new RankedPlayUserDisplay(new APIUser { Id = 1001, Username = "User 1001" }, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -36,7 +47,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
[Test]
public void TesUserDisplay()
{
AddStep("blue color scheme", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
AddStep("blue color scheme", () => Child = new RankedPlayUserDisplay(new APIUser { Id = 1001, Username = "User 1001" }, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -44,15 +55,30 @@ namespace osu.Game.Tests.Visual.RankedPlay
Health = { BindTarget = health }
});
AddStep("red color scheme", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Red)
AddStep("red color scheme", () => Child = new RankedPlayUserDisplay(new APIUser { Id = 1001, Username = "User 1001" }, Anchor.BottomLeft, RankedPlayColourScheme.Red)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(256, 72),
Health = { BindTarget = health }
});
}
AddSliderStep("health", 0, 1_000_000, 1_000_000, value => health.Value = value);
[Test]
public void TestBeatmapState()
{
float progress = 0;
AddStep("set unavailable", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()));
AddStep("set downloading", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress = 0)));
AddUntilStep("increment progress", () =>
{
progress += RNG.NextSingle(0.1f);
MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress));
return progress >= 1;
});
AddStep("set to importing", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Importing()));
AddStep("set to available", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()));
}
}
}
@@ -277,13 +277,18 @@ namespace osu.Game.Tests.Visual.Ranking
ScorePanel expandedPanel = null;
ScorePanel contractedPanel = null;
AddUntilStep("retrieve expanded panel",
() => expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded),
() => Is.Not.Null);
AddUntilStep("retrieve contracted panel",
() => contractedPanel = this.ChildrenOfType<ScorePanel>().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X),
() => Is.Not.Null);
AddStep("click expanded panel then contracted panel", () =>
{
expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
InputManager.MoveMouseTo(expandedPanel);
InputManager.Click(MouseButton.Left);
contractedPanel = this.ChildrenOfType<ScorePanel>().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X);
InputManager.MoveMouseTo(contractedPanel);
InputManager.Click(MouseButton.Left);
});
@@ -132,6 +132,7 @@ namespace osu.Game.Tests.Visual.Ranking
}
[Test]
[FlakyTest]
public void TestOnlineLeaderboardWithLessThan50Scores()
{
ScoreInfo localScore = null!;
@@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Settings
private readonly Bindable<float> current = new Bindable<float>
{
Default = default,
Default = 0,
Value = 1,
};
@@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Settings
foreach (var revertToDefaultButton in this.ChildrenOfType<RevertToDefaultButton<float>>())
revertToDefaultButton.Parent!.Scale = new Vector2(scale);
});
AddToggleStep("toggle default state", state => current.Value = state ? default : 1);
AddToggleStep("toggle default state", state => current.Value = state ? 0 : 1);
AddToggleStep("toggle disabled state", state => current.Disabled = state);
}
}
@@ -34,11 +34,11 @@ namespace osu.Game.Tests.Visual.SongSelect
AddBeatmaps(10, 3);
WaitForDrawablePanels();
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(1));
AddUntilStep("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(1));
ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(2));
AddUntilStep("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(2));
CheckDisplayedBeatmapSetsCount(1);
CheckDisplayedBeatmapsCount(3);
@@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.SongSelect
ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty);
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(3));
AddUntilStep("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(3));
CheckDisplayedBeatmapSetsCount(10);
CheckDisplayedBeatmapsCount(30);
@@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Screens.Select;
namespace osu.Game.Tests.Visual.SongSelect
@@ -37,7 +38,11 @@ namespace osu.Game.Tests.Visual.SongSelect
WaitForFiltering();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
() => Is.EqualTo(positionBefore).Using<Quad, Quad>((expected, actual)
=> Precision.AlmostEquals(expected.TopLeft, actual.TopLeft)
&& Precision.AlmostEquals(expected.TopRight, actual.TopRight)
&& Precision.AlmostEquals(expected.BottomLeft, actual.BottomLeft)
&& Precision.AlmostEquals(expected.BottomRight, actual.BottomRight)));
}
[Test]
@@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
[FlakyTest]
public void TestSetTraversal()
{
AddBeatmaps(3, splitApart: true);
@@ -127,6 +127,7 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
[FlakyTest]
public void TestBestRulesetIsRecommended()
{
BeatmapSetInfo osuSet = null, mixedSet = null;
@@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
using osu.Game.Screens.Select;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
@@ -27,6 +28,9 @@ namespace osu.Game.Tests.Visual.SongSelect
{
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
private DialogOverlay dialogOverlay = null!;
private RulesetStore rulesets = null!;
private BeatmapManager beatmapManager = null!;
@@ -11,6 +11,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Mods;
@@ -23,6 +24,7 @@ using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Leaderboards;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Tests.Resources;
using osuTK.Input;
using BeatmapCarousel = osu.Game.Screens.Select.BeatmapCarousel;
@@ -430,6 +432,31 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("osu! cookie visible", () => this.ChildrenOfType<OsuLogo>().Single().Alpha, () => Is.Not.Zero);
}
[Test]
public void TestDropdownKeyboardNavigation()
{
ImportBeatmapForRuleset(0);
LoadSongSelect();
BeatmapInfo? firstBeatmap = null;
AddStep("store first difficulty", () => firstBeatmap = Beatmap.Value.BeatmapInfo);
AddStep("click sort dropdown", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedDropdown<SortMode>>().Single());
InputManager.Click(MouseButton.Left);
});
AddStep("press up arrow", () => InputManager.Key(Key.Up));
AddStep("press up arrow", () => InputManager.Key(Key.Up));
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("sort mode is length", () => this.ChildrenOfType<ShearedDropdown<SortMode>>().Single().Current.Value, () => Is.EqualTo(SortMode.Length));
AddAssert("beatmap not changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(firstBeatmap));
}
#endregion
#region Footer
@@ -1,283 +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 System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
using osuTK.Input;
using Realms;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene
{
private RulesetStore rulesets = null!;
private BeatmapManager beatmapManager = null!;
private CollectionDropdown dropdown = null!;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
}
[SetUp]
public void SetUp() => Schedule(() =>
{
writeAndRefresh(r => r.RemoveAll<BeatmapCollection>());
Child = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = dropdown = new CollectionDropdown
{
Width = 300,
Y = 100,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
};
});
[Test]
public void TestEmptyCollectionFilterContainsAllBeatmaps()
{
assertCollectionDropdownContains(CollectionsStrings.AllBeatmaps);
assertCollectionHeaderDisplays(CollectionsStrings.AllBeatmaps);
}
[Test]
public void TestCollectionAddedToDropdown()
{
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
assertCollectionDropdownContains("1");
assertCollectionDropdownContains("2");
}
[Test]
public void TestCollectionsCleared()
{
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3"))));
AddUntilStep("check count 5", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(5));
AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll<BeatmapCollection>()));
AddUntilStep("check count 2", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(2));
}
[Test]
public void TestCollectionRemovedFromDropdown()
{
BeatmapCollection first = null!;
AddStep("add collection", () => writeAndRefresh(r => r.Add(first = new BeatmapCollection(name: "1"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
AddStep("remove collection", () => writeAndRefresh(r => r.Remove(first)));
assertCollectionDropdownContains("1", false);
assertCollectionDropdownContains("2");
}
[Test]
public void TestCollectionRenamed()
{
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
AddStep("select collection", () => dropdown.Current.Value = dropdown.ItemSource.ElementAt(1));
addExpandHeaderStep();
AddStep("change name", () => writeAndRefresh(_ => getFirstCollection().Name = "First"));
assertCollectionDropdownContains("First");
assertCollectionHeaderDisplays("First");
}
[Test]
public void TestAllBeatmapFilterDoesNotHaveAddButton()
{
addExpandHeaderStep();
AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0)));
AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent);
}
[Test]
public void TestCollectionFilterHasAddButton()
{
addExpandHeaderStep();
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1)));
AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent);
}
[Test]
public void TestButtonDisabledAndEnabledWithBeatmapChanges()
{
addExpandHeaderStep();
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value);
AddStep("set dummy beatmap", () => Beatmap.SetDefault());
AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value);
}
[Test]
public void TestButtonChangesWhenAddedAndRemovedFromCollection()
{
addExpandHeaderStep();
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash)));
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear()));
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
}
[Test]
public void TestButtonAddsAndRemovesBeatmap()
{
addExpandHeaderStep();
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
addClickAddOrRemoveButtonStep(1);
AddUntilStep("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
addClickAddOrRemoveButtonStep(1);
AddUntilStep("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
}
[Test]
public void TestManageCollectionsFilterIsNotSelected()
{
bool received = false;
addExpandHeaderStep();
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1", new List<string> { "abc" }))));
assertCollectionDropdownContains("1");
AddStep("select collection", () =>
{
InputManager.MoveMouseTo(getCollectionDropdownItemAt(1));
InputManager.Click(MouseButton.Left);
});
addExpandHeaderStep();
AddStep("watch for filter requests", () =>
{
received = false;
dropdown.ChildrenOfType<CollectionDropdown>().First().RequestFilter = () => received = true;
});
AddStep("click manage collections filter", () =>
{
int lastItemIndex = dropdown.ChildrenOfType<CollectionDropdown>().Single().Items.Count() - 1;
InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex));
InputManager.Click(MouseButton.Left);
});
AddAssert("collection filter still selected", () => dropdown.Current.Value.CollectionName == "1");
AddAssert("filter request not fired", () => !received);
}
private void writeAndRefresh(Action<Realm> action) => Realm.Write(r =>
{
action(r);
r.Refresh();
});
private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All<BeatmapCollection>().First());
private void assertCollectionHeaderDisplays(LocalisableString collectionName, bool shouldDisplay = true)
=> AddUntilStep($"collection dropdown header displays '{collectionName}'",
() => shouldDisplay == dropdown.ChildrenOfType<CollectionDropdown.OsuDropdownHeader>().Any(h => h.ChildrenOfType<SpriteText>().Any(t => t.Text == collectionName)));
private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon));
private void assertCollectionDropdownContains(LocalisableString collectionName, bool shouldContain = true) =>
AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
() => shouldContain == dropdown.ChildrenOfType<Menu.DrawableMenuItem>().Any(i => i.ChildrenOfType<CompositeDrawable>().OfType<IHasText>().First().Text == collectionName));
private IconButton getAddOrRemoveButton(int index)
=> getCollectionDropdownItemAt(index).ChildrenOfType<IconButton>().Single();
private void addExpandHeaderStep() => AddStep("expand header", () =>
{
InputManager.MoveMouseTo(dropdown.ChildrenOfType<CollectionDropdown.OsuDropdownHeader>().Single());
InputManager.Click(MouseButton.Left);
});
private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () =>
{
InputManager.MoveMouseTo(getAddOrRemoveButton(index));
InputManager.Click(MouseButton.Left);
});
private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index)
{
// todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079
CollectionFilterMenuItem item = dropdown.ChildrenOfType<CollectionDropdown>().Single().ItemSource.ElementAt(index);
return dropdown.ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value == item.CollectionName);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -11,13 +9,11 @@ using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses;
@@ -35,18 +31,17 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneDeleteLocalScore : OsuManualInputManagerTestScene
{
private readonly ContextMenuContainer contextMenuContainer;
private readonly BeatmapLeaderboardWedge leaderboard;
private RulesetStore rulesets = null!;
private BeatmapManager beatmapManager;
private ScoreManager scoreManager;
private BeatmapManager beatmapManager = null!;
private ScoreManager scoreManager = null!;
private readonly List<ScoreInfo> importedScores = new List<ScoreInfo>();
private BeatmapInfo beatmapInfo;
private BeatmapInfo beatmapInfo = null!;
private LeaderboardManager leaderboardManager { get; set; }
private LeaderboardManager leaderboardManager { get; set; } = null!;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
@@ -60,15 +55,11 @@ namespace osu.Game.Tests.Visual.UserInterface
{
Children = new Drawable[]
{
contextMenuContainer = new OsuContextMenuContainer
leaderboard = new BeatmapLeaderboardWedge
{
RelativeSizeAxes = Axes.Both,
Child = leaderboard = new BeatmapLeaderboardWedge
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(0.6f),
}
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(0.6f),
},
dialogOverlay = new DialogOverlay()
};
@@ -145,7 +136,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestDeleteViaRightClick()
{
ScoreInfo scoreBeingDeleted = null;
ScoreInfo scoreBeingDeleted = null!;
AddStep("open menu for top score", () =>
{
var leaderboardScore = leaderboard.ChildrenOfType<BeatmapLeaderboardScore>().First();
@@ -157,12 +148,12 @@ namespace osu.Game.Tests.Visual.UserInterface
});
// Ensure the context menu has finished showing
AddStep("finish transforms", () => contextMenuContainer.FinishTransforms(true));
AddStep("finish transforms", () => leaderboard.FinishTransforms(true));
AddStep("click delete option", () =>
{
InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>()
.First(i => string.Equals(i.Item.Text.Value.ToString(), "delete", System.StringComparison.OrdinalIgnoreCase)));
InputManager.MoveMouseTo(leaderboard.ChildrenOfType<DrawableOsuMenuItem>()
.First(i => string.Equals(i.Item.Text.Value.ToString(), "delete", System.StringComparison.OrdinalIgnoreCase)));
InputManager.Click(MouseButton.Left);
});
@@ -335,6 +335,50 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("wait overlay not present", () => !notificationOverlay.IsPresent);
}
[Test]
public void TestProgressSilentDismissal()
{
ProgressNotification notification = null!;
AddStep("add progress notification", () =>
{
notification = new ProgressNotification
{
Text = @"Uploading to BSS...",
CompletionText = "Uploaded to BSS!",
};
notificationOverlay.Post(notification);
progressingNotifications.Add(notification);
});
AddStep("silently dismiss", () => notification.CompleteSilently());
AddAssert("completed", () => notification.State == ProgressNotificationState.Completed);
AddAssert("Completion toast not shown", () => notificationOverlay.ToastCount == 0);
}
[Test]
public void TestProgressSilentDismissalImmediate()
{
ProgressNotification notification = null!;
AddStep("add progress notification", () =>
{
notification = new ProgressNotification
{
Text = @"Uploading to BSS...",
CompletionText = "Uploaded to BSS!",
};
notification.CompleteSilently();
notificationOverlay.Post(notification);
progressingNotifications.Add(notification);
});
AddAssert("completed", () => notification.State == ProgressNotificationState.Completed);
AddAssert("Completion toast not shown", () => notificationOverlay.ToastCount == 0);
}
[Test]
public void TestProgressClick()
{
@@ -13,11 +13,13 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Select;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
@@ -142,6 +144,44 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("other buttons returned", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.ChildrenOfType<Container>().First().Y == 0));
}
[Test]
public void TestButtonsHiddenByExternalOverlayContentCannotBeTriggered()
{
TestScreen screen = null!;
bool buttonTriggered = false;
AddStep("push screen", () =>
{
ShearedOverlayContainer overlay = new TestShearedOverlayContainer();
LoadScreen(screen = new TestScreen
{
Overlay = overlay,
CreateButtons = () => new[]
{
new ScreenFooterButton(overlay)
{
AccentColour = Dependencies.Get<OsuColour>().Orange1,
Icon = FontAwesome.Solid.Toolbox,
Text = "One",
},
new ScreenFooterButton { Text = "Two", Hotkey = GlobalAction.Select, Action = () => buttonTriggered = true },
new ScreenFooterButton { Text = "Three", Action = () => { } },
},
});
});
AddUntilStep("wait until screen is loaded", () => screen.IsCurrentScreen(), () => Is.True);
AddStep("show overlay", () => screen.Overlay.Show());
contentDisplayed();
AddStep("try direct click", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().ElementAt(1).TriggerClick());
AddAssert("action not triggered", () => buttonTriggered, () => Is.False);
AddStep("try hotkey", () => InputManager.Key(Key.Enter));
AddAssert("action not triggered", () => buttonTriggered, () => Is.False);
}
[Test]
public void TestTemporarilyShowFooter()
{
@@ -114,51 +114,6 @@ namespace osu.Game.Tests.Visual.UserInterface
=> AddAssert($"state is {expected}", () => state.Value == expected);
}
[Test]
public void TestItemRespondsToRightClick()
{
OsuMenu menu = null;
Bindable<TernaryState> state = new Bindable<TernaryState>(TernaryState.Indeterminate);
AddStep("create menu", () =>
{
state.Value = TernaryState.Indeterminate;
Child = menu = new OsuMenu(Direction.Vertical, true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Items = new[]
{
new TernaryStateToggleMenuItem("First"),
new TernaryStateToggleMenuItem("Second") { State = { BindTarget = state } },
new TernaryStateToggleMenuItem("Third") { State = { Value = TernaryState.True } },
}
};
});
checkState(TernaryState.Indeterminate);
click();
checkState(TernaryState.True);
click();
checkState(TernaryState.False);
AddStep("change state via bindable", () => state.Value = TernaryState.True);
void click() =>
AddStep("click", () =>
{
InputManager.MoveMouseTo(menu.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Right);
});
void checkState(TernaryState expected)
=> AddAssert($"state is {expected}", () => state.Value == expected);
}
[Test]
public void TestCustomState()
{
@@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.UserInterface
OsuSpriteText sort;
OsuSpriteText displayStyle;
Add(toolbar = new UserListToolbar(true)
Add(toolbar = new UserListToolbar
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -10,5 +10,9 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="ppy.LocalisationAnalyser" Version="2025.1208.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
+4
View File
@@ -22,6 +22,8 @@ namespace osu.Game.Audio
protected TrackManagerPreviewTrack? CurrentTrack;
public readonly BindableBool IsPlayingPreview = new BindableBool();
public PreviewTrackManager(IAdjustableAudioComponent mainTrackAdjustments)
{
this.mainTrackAdjustments = mainTrackAdjustments;
@@ -47,6 +49,7 @@ namespace osu.Game.Audio
CurrentTrack?.Stop();
CurrentTrack = track;
mainTrackAdjustments.AddAdjustment(AdjustableProperty.Volume, muteBindable);
IsPlayingPreview.Value = true;
});
track.Stopped += () => Schedule(() =>
@@ -56,6 +59,7 @@ namespace osu.Game.Audio
CurrentTrack = null;
mainTrackAdjustments.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
IsPlayingPreview.Value = false;
});
return track;
+40 -17
View File
@@ -137,10 +137,15 @@ namespace osu.Game.Beatmaps
Value = new StarDifficulty(beatmapInfo.StarRating, 0)
};
updateBindable(bindable, currentRuleset.Value, currentMods.Value, cancellationToken, computationDelay);
lock (bindableUpdateLock)
{
var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, cancellationToken);
linkedCancellationSources.Add(linkedSource);
updateBindable(bindable, currentRuleset.Value, currentMods.Value, linkedSource, computationDelay);
trackedBindables.Add(bindable);
}
return bindable;
}
@@ -212,7 +217,7 @@ namespace osu.Game.Beatmaps
var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken);
linkedCancellationSources.Add(linkedSource);
updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token);
updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource);
}
}
}
@@ -243,27 +248,45 @@ namespace osu.Game.Beatmaps
/// <param name="bindable">The <see cref="BindableStarDifficulty"/> to update.</param>
/// <param name="rulesetInfo">The <see cref="IRulesetInfo"/> to update with.</param>
/// <param name="mods">The <see cref="Mod"/>s to update with.</param>
/// <param name="cancellationToken">A token that may be used to cancel this update.</param>
/// <param name="linkedCancellationTokenSource">
/// A cancellation token source that may be used to cancel this update.
/// This token will be cancelled in one of two scenarios:
/// <list type="bullet">
/// <item>The owner of the bindable has requested the cancellation.</item>
/// <item>An <see cref="Invalidate"/> call has been issued, and as such ongoing calculations must be aborted to avoid stale values being potentially written to bindables.</item>
/// </list>
/// </param>
/// <param name="computationDelay">In the case a cached lookup was not possible, a value in milliseconds of to wait until performing potentially intensive lookup.</param>
private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rulesetInfo, IEnumerable<Mod>? mods, CancellationToken cancellationToken = default, int computationDelay = 0)
private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rulesetInfo, IEnumerable<Mod>? mods, CancellationTokenSource linkedCancellationTokenSource, int computationDelay = 0)
{
// GetDifficultyAsync will fall back to existing data from IBeatmapInfo if not locally available
// (contrary to GetAsync)
GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, cancellationToken, computationDelay)
GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, linkedCancellationTokenSource.Token, computationDelay)
.ContinueWith(task =>
{
// We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events.
Schedule(() =>
{
if (cancellationToken.IsCancellationRequested)
return;
// We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events.
Schedule(() =>
{
if (!linkedCancellationTokenSource.IsCancellationRequested)
{
StarDifficulty? starDifficulty = task.GetResultSafely();
StarDifficulty? starDifficulty = task.GetResultSafely();
if (starDifficulty != null)
bindable.Value = starDifficulty.Value;
}
if (starDifficulty != null)
bindable.Value = starDifficulty.Value;
});
}, cancellationToken);
// Once the linked cancellation token source is of no remaining use to anybody, clean it up.
lock (bindableUpdateLock)
{
linkedCancellationSources.Remove(linkedCancellationTokenSource);
linkedCancellationTokenSource.Dispose();
}
});
},
// This continuation MUST run even if the antecedent `GetDifficultyAsync()` call was canceled in order to clean up `linkedCancellationTokenSource`.
// Due to this, `ContinueWith()` CANNOT accept `linkedCancellationTokenSource.Token` here, because if it did, then in an event of a cancellation,
// the continuation would never be scheduled for execution.
CancellationToken.None);
}
/// <summary>
@@ -424,7 +447,7 @@ namespace osu.Game.Beatmaps
Stream IWorkingBeatmap.GetStream(string storagePath) => working.GetStream(storagePath);
void IWorkingBeatmap.BeginAsyncLoad() => working.BeginAsyncLoad();
void IWorkingBeatmap.CancelAsyncLoad() => working.CancelAsyncLoad();
void IWorkingBeatmap.PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint) => working.PrepareTrackForPreview(looping, offsetFromPreviewPoint);
void IWorkingBeatmap.PrepareTrackForPreview(bool looping, double? offsetFromPreviewPoint) => working.PrepareTrackForPreview(looping, offsetFromPreviewPoint);
}
}
}
+1 -1
View File
@@ -296,7 +296,7 @@ namespace osu.Game.Beatmaps
return Realm.Run(r =>
{
r.Refresh();
return r.All<BeatmapSetInfo>().Where(b => !b.DeletePending).Detach();
return r.All<BeatmapSetInfo>().Where(b => !b.DeletePending).AsEnumerable().Detach();
});
}
+2 -1
View File
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging;
@@ -36,7 +37,7 @@ namespace osu.Game.Beatmaps
public void Queue(Live<BeatmapSetInfo> beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
{
Logger.Log($"Queueing change for local beatmap {beatmapSet}");
Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, lookupScope)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously,
Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, lookupScope)), CancellationToken.None, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously,
updateScheduler);
}
+1 -1
View File
@@ -139,6 +139,6 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
/// </summary>
void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0);
void PrepareTrackForPreview(bool looping, double? offsetFromPreviewPoint = null);
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Beatmaps
{
public readonly struct StarDifficulty
public readonly record struct StarDifficulty
{
/// <summary>
/// The star difficulty rating for the given beatmap.
+6 -3
View File
@@ -16,6 +16,7 @@ using osu.Framework.Audio.Track;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
@@ -119,7 +120,7 @@ namespace osu.Game.Beatmaps
return track;
}
public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0)
public void PrepareTrackForPreview(bool looping, double? offsetFromPreviewPoint = null)
{
Track.Looping = looping;
Track.RestartPoint = Metadata.PreviewTime;
@@ -133,7 +134,9 @@ namespace osu.Game.Beatmaps
if (Track.RestartPoint < 0 || Track.RestartPoint > Track.Length)
Track.RestartPoint = 0.4f * Track.Length;
Track.RestartPoint = Math.Clamp(Track.RestartPoint + offsetFromPreviewPoint, 0, Track.Length);
offsetFromPreviewPoint ??= -MusicController.DELAY_BEFORE_FADE;
Track.RestartPoint = Math.Clamp(Track.RestartPoint + offsetFromPreviewPoint.Value, 0, Track.Length);
}
/// <summary>
@@ -262,7 +265,7 @@ namespace osu.Game.Beatmaps
using (var cancellationTokenSource = new CancellationTokenSource(10_000))
{
// don't apply the default timeout when debugger is attached (may be breakpointing / debugging).
return GetPlayableBeatmap(ruleset, mods ?? Array.Empty<Mod>(), Debugger.IsAttached ? new CancellationToken() : cancellationTokenSource.Token);
return GetPlayableBeatmap(ruleset, mods ?? Array.Empty<Mod>(), Debugger.IsAttached ? CancellationToken.None : cancellationTokenSource.Token);
}
}
catch (OperationCanceledException)
-283
View File
@@ -1,283 +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.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osuTK;
using Realms;
namespace osu.Game.Collections
{
/// <summary>
/// A dropdown to select the collection to be used to filter results.
/// WARNING: TODO: we have TWO `CollectionDropdowns` with diverging functionality. This is not good.
/// </summary>
public partial class CollectionDropdown : OsuDropdown<CollectionFilterMenuItem>
{
/// <summary>
/// Whether to show the "manage collections..." menu item in the dropdown.
/// </summary>
protected virtual bool ShowManageCollectionsItem => true;
public Action? RequestFilter { private get; set; }
private readonly BindableList<CollectionFilterMenuItem> filters = new BindableList<CollectionFilterMenuItem>();
[Resolved]
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
[Resolved]
private RealmAccess realm { get; set; } = null!;
private IDisposable? realmSubscription;
private readonly CollectionFilterMenuItem allBeatmapsItem = new AllBeatmapsCollectionFilterMenuItem();
public CollectionDropdown()
{
ItemSource = filters;
Current.Value = allBeatmapsItem;
AlwaysShowSearchBar = true;
}
protected override void LoadComplete()
{
base.LoadComplete();
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapCollection>().OrderBy(c => c.Name), collectionsChanged);
Current.BindValueChanged(selectionChanged);
}
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes)
{
if (changes == null)
{
filters.Clear();
filters.Add(allBeatmapsItem);
filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm))));
if (ShowManageCollectionsItem)
filters.Add(new ManageCollectionsFilterMenuItem());
}
else
{
foreach (int i in changes.DeletedIndices.OrderDescending())
filters.RemoveAt(i + 1);
foreach (int i in changes.InsertedIndices)
filters.Insert(i + 1, new CollectionFilterMenuItem(collections[i].ToLive(realm)));
var selectedItem = SelectedItem?.Value;
foreach (int i in changes.NewModifiedIndices)
{
var updatedItem = collections[i];
// This is responsible for updating the state of the +/- button and the collection's name.
// TODO: we can probably make the menu items update with changes to avoid this.
filters.RemoveAt(i + 1);
filters.Insert(i + 1, new CollectionFilterMenuItem(updatedItem.ToLive(realm)));
if (updatedItem.ID == selectedItem?.Collection?.ID)
{
// This current update and schedule is required to work around dropdown headers not updating text even when the selected item
// changes. It's not great but honestly the whole dropdown menu structure isn't great. This needs to be fixed, but I'll issue
// a warning that it's going to be a frustrating journey.
Current.Value = allBeatmapsItem;
Schedule(() =>
{
// current may have changed before the scheduled call is run.
if (Current.Value != allBeatmapsItem)
return;
Current.Value = filters.SingleOrDefault(f => f.Collection?.ID == selectedItem.Collection?.ID) ?? filters[0];
});
// Trigger an external re-filter if the current item was in the change set.
RequestFilter?.Invoke();
break;
}
}
}
}
private Live<BeatmapCollection>? lastFiltered;
private void selectionChanged(ValueChangedEvent<CollectionFilterMenuItem> filter)
{
// May be null during .Clear().
if (filter.NewValue.IsNull())
return;
// Never select the manage collection filter - rollback to the previous filter.
// This is done after the above since it is important that bindable is unbound from OldValue, which is lost after forcing it back to the old value.
if (filter.NewValue is ManageCollectionsFilterMenuItem)
{
Current.Value = filter.OldValue;
manageCollectionsDialog?.Show();
return;
}
var newCollection = filter.NewValue.Collection;
// This dropdown be weird.
// We only care about filtering if the actual collection has changed.
if (newCollection != lastFiltered)
{
RequestFilter?.Invoke();
lastFiltered = newCollection;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
realmSubscription?.Dispose();
}
protected override LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName;
protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader();
protected sealed override DropdownMenu CreateMenu() => CreateCollectionMenu();
protected virtual CollectionDropdownHeader CreateCollectionHeader() => new CollectionDropdownHeader();
protected virtual CollectionDropdownMenu CreateCollectionMenu() => new CollectionDropdownMenu();
public partial class CollectionDropdownHeader : OsuDropdownHeader
{
public CollectionDropdownHeader()
{
Height = 25;
Chevron.Size = new Vector2(12);
Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 8 };
}
}
protected partial class CollectionDropdownMenu : OsuDropdownMenu
{
public CollectionDropdownMenu()
{
MaxHeight = 200;
}
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownDrawableMenuItem(item)
{
BackgroundColourHover = HoverColour,
BackgroundColourSelected = SelectionColour
};
}
protected partial class CollectionDropdownDrawableMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem
{
private IconButton addOrRemoveButton = null!;
private bool beatmapInCollection;
private readonly Live<BeatmapCollection>? collection;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
public CollectionDropdownDrawableMenuItem(MenuItem item)
: base(item)
{
collection = ((DropdownMenuItem<CollectionFilterMenuItem>)item).Value.Collection;
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(addOrRemoveButton = new NoFocusChangeIconButton
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
X = -OsuScrollContainer.SCROLL_BAR_WIDTH,
Scale = new Vector2(0.65f),
Action = addOrRemove,
});
}
protected override void LoadComplete()
{
base.LoadComplete();
if (collection != null)
{
beatmap.BindValueChanged(_ =>
{
beatmapInCollection = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.Value.BeatmapInfo.MD5Hash));
addOrRemoveButton.Enabled.Value = !beatmap.IsDefault;
addOrRemoveButton.Icon = beatmapInCollection ? FontAwesome.Solid.MinusSquare : FontAwesome.Solid.PlusSquare;
addOrRemoveButton.TooltipText = beatmapInCollection ? "Remove selected beatmap" : "Add selected beatmap";
updateButtonVisibility();
}, true);
}
updateButtonVisibility();
}
protected override bool OnHover(HoverEvent e)
{
updateButtonVisibility();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateButtonVisibility();
base.OnHoverLost(e);
}
protected override void OnSelectChange()
{
base.OnSelectChange();
updateButtonVisibility();
}
private void updateButtonVisibility()
{
if (collection == null)
addOrRemoveButton.Alpha = 0;
else
addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0;
}
private void addOrRemove()
{
Debug.Assert(collection != null);
Task.Run(() => collection.PerformWrite(c =>
{
if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash))
c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash);
}));
}
protected override Drawable CreateContent() => (Content)base.CreateContent();
private partial class NoFocusChangeIconButton : IconButton
{
public override bool ChangeFocusOnClick => false;
}
}
}
}
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -655,7 +656,8 @@ namespace osu.Game.Database
if (metadataSourceFetchDate <= lastPopulation)
{
Logger.Log($@"Skipping user tag population because the local metadata source hasn't been updated since the last time user tags were checked ({lastPopulation.Value:d})");
Logger.Log(
$@"Skipping user tag population because the local metadata source hasn't been updated since the last time user tags were checked ({lastPopulation.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)})");
return;
}
@@ -675,9 +677,11 @@ namespace osu.Game.Database
Logger.Log($@"Found {beatmapIds.Count} beatmaps with missing user tags.");
var notification = showProgressNotification(beatmapIds.Count, @"Populating missing user tags", @"beatmaps have had their tags updated.");
var notification = showProgressNotification(beatmapIds.Count, @"Populating missing user tags",
@"beatmaps have had their tags updated. This runs once a month to allow searching user tags.");
int processedCount = 0;
int updatedCount = 0;
int failedCount = 0;
foreach (var id in beatmapIds)
@@ -691,33 +695,37 @@ namespace osu.Game.Database
try
{
// Can't use async overload because we're not on the update thread.
// ReSharper disable once MethodHasAsyncOverload
realmAccess.Write(r =>
var beatmap = realmAccess.Run(r => r.Find<BeatmapInfo>(id)?.Detach());
if (beatmap == null) continue;
bool lookupSucceeded = localMetadataSource.TryLookup(beatmap, out var result);
if (lookupSucceeded)
{
BeatmapInfo beatmap = r.Find<BeatmapInfo>(id)!;
Debug.Assert(result != null);
bool lookupSucceeded = localMetadataSource.TryLookup(beatmap, out var result);
HashSet<string> userTags = result.UserTags.ToHashSet();
if (lookupSucceeded)
if (!userTags.SetEquals(beatmap.Metadata.UserTags))
{
Debug.Assert(result != null);
var userTags = result.UserTags.ToHashSet();
if (!userTags.SetEquals(beatmap.Metadata.UserTags))
++updatedCount;
realmAccess.Write(r =>
{
beatmap = r.Find<BeatmapInfo>(id);
if (beatmap == null)
return;
beatmap.Metadata.UserTags.Clear();
beatmap.Metadata.UserTags.AddRange(userTags);
return true;
}
return false;
});
}
}
else
{
Logger.Log(@$"Could not find {beatmap.GetDisplayString()} in local cache while backpopulating missing user tags");
return false;
});
}
++processedCount;
}
@@ -732,7 +740,9 @@ namespace osu.Game.Database
}
}
completeNotification(notification, processedCount, beatmapIds.Count, failedCount);
// Report the updated item count rather than the total processed. Users don't really care about noops here.
completeNotification(notification, updatedCount, updatedCount, failedCount);
config.SetValue(OsuSetting.LastOnlineTagsPopulation, metadataSourceFetchDate);
}
@@ -753,7 +763,11 @@ namespace osu.Game.Database
if (notification == null)
return;
if (processedCount == totalCount)
if (totalCount == 0)
{
notification.CompleteSilently();
}
else if (processedCount == totalCount)
{
notification.CompletionText = $"{processedCount} {notification.CompletionText}";
notification.Progress = 1;
@@ -71,8 +71,15 @@ namespace osu.Game.Database
// Encode to legacy format
var stream = new MemoryStream();
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
// Maintain line endings in windows style.
// If we don't do that, uploads to BSS may show changes where there are none.
sw.NewLine = "\r\n";
new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw);
}
stream.Seek(0, SeekOrigin.Begin);
@@ -81,6 +88,16 @@ namespace osu.Game.Database
protected virtual void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap)
{
// Limit grid sizes to those which stable knows about.
if (playableBeatmap.GridSize >= 24)
playableBeatmap.GridSize = 32;
else if (playableBeatmap.GridSize >= 12)
playableBeatmap.GridSize = 16;
else if (playableBeatmap.GridSize >= 6)
playableBeatmap.GridSize = 8;
else
playableBeatmap.GridSize = 4;
// Convert beatmap elements to be compatible with legacy format
// So we truncate time and position values to integers, and convert paths with multiple segments to Bézier curves
+1 -1
View File
@@ -1227,7 +1227,7 @@ namespace osu.Game.Database
var oldKeyBindingsQuery = migration.NewRealm
.All<RealmKeyBinding>()
.Where(kb => kb.RulesetName == @"mania" && kb.Variant == variant);
var oldKeyBindings = oldKeyBindingsQuery.Detach();
var oldKeyBindings = oldKeyBindingsQuery.AsEnumerable().Detach();
migration.NewRealm.RemoveRange(oldKeyBindingsQuery);
+2 -2
View File
@@ -55,9 +55,9 @@ namespace osu.Game.Extensions
{
tcs.TrySetResult(true);
}
}, cancellationToken: default);
}, cancellationToken: CancellationToken.None);
}
}, cancellationToken: default);
}, cancellationToken: CancellationToken.None);
// importantly, we are not returning the continuation itself but rather a task which represents its status in sequential execution order.
// this will not be cancelled or completed until the previous task has also.
@@ -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.Threading;
@@ -21,10 +22,15 @@ namespace osu.Game.Graphics.Backgrounds
{
private readonly InterpolatingFramedClock storyboardClock;
private AudioContainer storyboardContainer = null!;
public readonly AudioContainer Storyboard;
private DrawableStoryboard? drawableStoryboard;
private CancellationTokenSource? loadCancellationSource = new CancellationTokenSource();
public Action? StoryboardLoaded { get; set; }
public readonly BindableBool ShowStoryboard = new BindableBool(true);
[Resolved(CanBeNull = true)]
private MusicController? musicController { get; set; }
@@ -35,17 +41,17 @@ namespace osu.Game.Graphics.Backgrounds
: base(beatmap, fallbackTextureName)
{
storyboardClock = new InterpolatingFramedClock();
AddInternal(Storyboard = new AudioContainer
{
RelativeSizeAxes = Axes.Both,
Volume = { Value = 0 },
});
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(storyboardContainer = new AudioContainer
{
RelativeSizeAxes = Axes.Both,
Volume = { Value = 0 },
});
LoadStoryboard(false);
}
@@ -71,11 +77,11 @@ namespace osu.Game.Graphics.Backgrounds
void finishLoad(DrawableStoryboard s)
{
if (Beatmap.Storyboard.ReplacesBackground)
Sprite.FadeOut(BackgroundScreen.TRANSITION_LENGTH, Easing.InQuint);
Storyboard.FadeInFromZero(BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint);
Storyboard.Add(s);
storyboardContainer.FadeInFromZero(BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint);
storyboardContainer.Add(s);
StoryboardLoaded?.Invoke();
updateStoryboardVisibility();
}
}
@@ -88,11 +94,10 @@ namespace osu.Game.Graphics.Backgrounds
loadCancellationSource = null;
// clear is intentionally used here for the storyboard to be disposed asynchronously.
storyboardContainer.Clear();
Storyboard.Clear();
drawableStoryboard = null;
Sprite.Alpha = 1f;
updateStoryboardVisibility();
}
protected override void LoadComplete()
@@ -102,6 +107,17 @@ namespace osu.Game.Graphics.Backgrounds
musicController.TrackChanged += onTrackChanged;
updateStoryboardClockSource(Beatmap);
ShowStoryboard.BindValueChanged(_ => updateStoryboardVisibility(), true);
}
private void updateStoryboardVisibility()
{
bool showStoryboard = drawableStoryboard != null && ShowStoryboard.Value;
bool showBackground = !showStoryboard || !Beatmap.Storyboard.ReplacesBackground;
Storyboard.FadeTo(showStoryboard ? 1 : 0, BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint);
Sprite.FadeTo(showBackground ? 1 : 0, BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint);
}
private void onTrackChanged(WorkingBeatmap newBeatmap, TrackChangeDirection _) => updateStoryboardClockSource(newBeatmap);
+1 -1
View File
@@ -66,7 +66,7 @@ namespace osu.Game.Graphics.Backgrounds
/// <summary>
/// The amount of triangles we want compared to the default distribution.
/// </summary>
protected virtual float SpawnRatio => 1;
public float SpawnRatio { get; set; } = 1;
private readonly BindableFloat triangleScale = new BindableFloat(1f);
@@ -35,7 +35,7 @@ namespace osu.Game.Graphics.Containers
{
}
protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = default)
protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = null)
{
UserScrolling = true;
base.OnUserScroll(value, animated, distanceDecay);
+5 -5
View File
@@ -688,11 +688,11 @@ namespace osu.Game.Graphics
public class Glyph : ITexturedCharacterGlyph
{
public float XOffset => default;
public float YOffset => default;
public float XAdvance => default;
public float Baseline => default;
public char Character => default;
public float XOffset => 0;
public float YOffset => 0;
public float XAdvance => 0;
public float Baseline => 0;
public char Character => '\0';
public float GetKerning<T>(T lastGlyph) where T : ICharacterGlyph => throw new NotImplementedException();
@@ -4,9 +4,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osuTK;
using osuTK.Input;
namespace osu.Game.Graphics.UserInterface
{
@@ -14,6 +12,8 @@ namespace osu.Game.Graphics.UserInterface
{
protected new StatefulMenuItem Item => (StatefulMenuItem)base.Item;
public override bool CloseMenuOnClick => false;
public DrawableStatefulMenuItem(StatefulMenuItem item)
: base(item)
{
@@ -21,19 +21,6 @@ namespace osu.Game.Graphics.UserInterface
protected override TextContainer CreateTextContainer() => new ToggleTextContainer(Item);
protected override bool OnMouseDown(MouseDownEvent e)
{
// Right mouse button is a special case where we allow actioning without dismissing the menu.
// This is achieved by not calling `Clicked` (as done by the base implementation in OnClick).
if (IsActionable && e.Button == MouseButton.Right)
{
Item.Action.Value?.Invoke();
return true;
}
return false;
}
private partial class ToggleTextContainer : TextContainer
{
private readonly StatefulMenuItem menuItem;
+21
View File
@@ -1,6 +1,8 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Input;
@@ -55,5 +57,24 @@ namespace osu.Game.Graphics.UserInterface
return result;
}
public bool Equals(Hotkey other)
{
if (KeyCombinations == null && other.KeyCombinations != null)
return false;
if (KeyCombinations != null && other.KeyCombinations == null)
return false;
bool result = (KeyCombinations == null && other.KeyCombinations == null) || KeyCombinations!.SequenceEqual(other.KeyCombinations!);
result &= GlobalAction == other.GlobalAction;
result &= PlatformAction == other.PlatformAction;
return result;
}
public override int GetHashCode()
{
return HashCode.Combine(StructuralComparisons.StructuralEqualityComparer.GetHashCode(KeyCombinations ?? []), GlobalAction, PlatformAction);
}
}
}

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