mirror of
https://github.com/ppy/osu.git
synced 2026-05-13 22:43:24 +08:00
Compare commits
78 Commits
pp-dev
...
2026.408.0
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
Vendored
+13
@@ -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
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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 = "";
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRelativeImage()
|
||||
{
|
||||
AddStep("Add relative image", () =>
|
||||
{
|
||||
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
|
||||
markdownContainer.Text = "";
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBlockImage()
|
||||
{
|
||||
AddStep("Add paragraph with block image", () =>
|
||||
{
|
||||
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
|
||||
markdownContainer.Text = @"Line before image
|
||||
|
||||

|
||||
|
||||
Line after image";
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInlineImage()
|
||||
{
|
||||
AddStep("Add inline image", () =>
|
||||
{
|
||||
markdownContainer.CurrentPath = "https://dev.ppy.sh";
|
||||
markdownContainer.Text = " osu!";
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTableWithImageContent()
|
||||
{
|
||||
AddStep("Add Table", () =>
|
||||
{
|
||||
markdownContainer.CurrentPath = "https://dev.ppy.sh";
|
||||
markdownContainer.Text = @"
|
||||
| Image | Name | Effect |
|
||||
| :-: | :-: | :-- |
|
||||
|  | 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. |
|
||||
|  | (激) 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. |
|
||||
|  | 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. |
|
||||
|   | (喝) 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. |
|
||||
|  | 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. |
|
||||
|  | 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 = "";
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user