mirror of
https://github.com/ppy/osu.git
synced 2026-05-14 04:22:34 +08:00
Compare commits
30 Commits
@@ -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'
|
||||
|
||||
+20
-15
@@ -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.11
|
||||
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"
|
||||
|
||||
@@ -98,7 +103,7 @@ jobs:
|
||||
# Attempt to upload results even if test fails.
|
||||
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
|
||||
- name: Upload Test Results
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
|
||||
@@ -110,16 +115,16 @@ jobs:
|
||||
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 +140,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: |
|
||||
|
||||
@@ -22,13 +22,13 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
|
||||
- name: Download results
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: osu-test-results-*
|
||||
merge-multiple: true
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Annotate CI run with test results
|
||||
uses: dorny/test-reporter@v1.8.0
|
||||
uses: dorny/test-reporter@v2.6.0
|
||||
with:
|
||||
name: Results
|
||||
path: "*.trx"
|
||||
|
||||
@@ -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
|
||||
|
||||
+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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -439,6 +441,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()
|
||||
{
|
||||
|
||||
@@ -56,6 +56,25 @@ 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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -424,7 +424,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.TRACK_FADE_IN_TIME;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,13 @@ 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; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private MusicController? musicController { get; set; }
|
||||
|
||||
@@ -35,17 +39,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);
|
||||
}
|
||||
|
||||
@@ -74,8 +78,10 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
if (Beatmap.Storyboard.ReplacesBackground)
|
||||
Sprite.FadeOut(BackgroundScreen.TRANSITION_LENGTH, Easing.InQuint);
|
||||
|
||||
storyboardContainer.FadeInFromZero(BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint);
|
||||
storyboardContainer.Add(s);
|
||||
Storyboard.FadeInFromZero(BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint);
|
||||
Storyboard.Add(s);
|
||||
|
||||
StoryboardLoaded?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +94,7 @@ 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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@@ -14,8 +15,20 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class SectionHeader : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Extra text to be shown in brackets next to the header.
|
||||
/// Unlike the header itself, this can be updated during runtime.
|
||||
/// </summary>
|
||||
public readonly Bindable<string> DetailsText = new Bindable<string>();
|
||||
|
||||
private readonly LocalisableString text;
|
||||
|
||||
private OsuTextFlowContainer textFlow = null!;
|
||||
private ITextPart? detailsPart;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public SectionHeader(LocalisableString text)
|
||||
{
|
||||
this.text = text;
|
||||
@@ -27,7 +40,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
@@ -37,7 +50,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Spacing = new Vector2(2),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold))
|
||||
textFlow = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold))
|
||||
{
|
||||
Text = text,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
@@ -51,5 +64,21 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
DetailsText.BindValueChanged(updateDetails, true);
|
||||
}
|
||||
|
||||
private void updateDetails(ValueChangedEvent<string> details)
|
||||
{
|
||||
if (detailsPart != null)
|
||||
textFlow.RemovePart(detailsPart);
|
||||
|
||||
if (!string.IsNullOrEmpty(details.NewValue))
|
||||
detailsPart = textFlow.AddText($" ({details.NewValue})", t => t.Colour = colourProvider.Highlight1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace osu.Game.Input.Bindings
|
||||
{
|
||||
var defaults = DefaultKeyBindings.ToList();
|
||||
|
||||
List<RealmKeyBinding> newBindings = realmKeyBindings.Detach()
|
||||
List<RealmKeyBinding> newBindings = realmKeyBindings.AsEnumerable().Detach()
|
||||
// this ordering is important to ensure that we read entries from the database in the order
|
||||
// enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
|
||||
// have been eaten by the music controller due to query order.
|
||||
|
||||
@@ -235,6 +235,11 @@ Click to see what's new!", version);
|
||||
/// </summary>
|
||||
public static LocalisableString ElevatedPrivileges(LocalisableString user) => new TranslatableString(getKey(@"elevated_privileges"), @"Running osu! as {0} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.", user);
|
||||
|
||||
/// <summary>
|
||||
/// "On macOS, installing osu! to a directory other than /Applications or {0}/Applications can cause issues with updating the game. Please move your game installation to one of these locations."
|
||||
/// </summary>
|
||||
public static LocalisableString MacOSAppLocation(LocalisableString userProfile) => new TranslatableString(getKey(@"macos_app_location"), @"On macOS, installing osu! to a directory other than /Applications or {0}/Applications can cause issues with updating the game. Please move your game installation to one of these locations.", userProfile);
|
||||
|
||||
/// <summary>
|
||||
/// "Screenshot saved! Click to view.
|
||||
/// {0}"
|
||||
|
||||
@@ -49,6 +49,31 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString PlaylistTrayDescription => new TranslatableString(getKey(@"playlist_tray_description"), @"Manage items on previous screen");
|
||||
|
||||
/// <summary>
|
||||
/// "Beatmap queue"
|
||||
/// </summary>
|
||||
public static LocalisableString MultiplayerBeatmapQueue => new TranslatableString(getKey(@"multiplayer_beatmap_queue"), @"Beatmap queue");
|
||||
|
||||
/// <summary>
|
||||
/// "Progress"
|
||||
/// </summary>
|
||||
public static LocalisableString PlaylistProgress => new TranslatableString(getKey(@"playlist_progress"), @"Progress");
|
||||
|
||||
/// <summary>
|
||||
/// "Leaderboard"
|
||||
/// </summary>
|
||||
public static LocalisableString PlaylistLeaderboard => new TranslatableString(getKey(@"playlist_leaderboard"), @"Leaderboard");
|
||||
|
||||
/// <summary>
|
||||
/// "Difficulty"
|
||||
/// </summary>
|
||||
public static LocalisableString Difficulty => new TranslatableString(getKey(@"difficulty"), @"Difficulty");
|
||||
|
||||
/// <summary>
|
||||
/// "Chat"
|
||||
/// </summary>
|
||||
public static LocalisableString Chat => new TranslatableString(getKey(@"chat"), @"Chat");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,11 @@ Leaderboards may be reset.");
|
||||
public static LocalisableString QualifiedBeatmapDisclaimerContent => new TranslatableString(getKey(@"qualified_beatmap_disclaimer_content"), @"No performance points will be awarded.
|
||||
Leaderboards will be reset when the beatmap is ranked.");
|
||||
|
||||
/// <summary>
|
||||
/// "Loading paused..."
|
||||
/// </summary>
|
||||
public static LocalisableString LoadingPaused => new TranslatableString(getKey(@"loading_paused"), @"Loading paused...");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
// Generally this is required because this model may be used by server-side components, but
|
||||
// we don't want to bother sending these fields in score submission requests, for instance.
|
||||
public bool ShouldSerializeEndedAt() => EndedAt != default;
|
||||
public bool ShouldSerializeStartedAt() => StartedAt != default;
|
||||
public bool ShouldSerializeStartedAt() => StartedAt != null;
|
||||
public bool ShouldSerializeLegacyScoreId() => LegacyScoreId != null;
|
||||
public bool ShouldSerializeLegacyTotalScore() => LegacyTotalScore != null;
|
||||
public bool ShouldSerializeMods() => Mods.Length > 0;
|
||||
|
||||
@@ -6,19 +6,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Chat.Listing;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
|
||||
namespace osu.Game.Online.Chat
|
||||
{
|
||||
@@ -70,6 +81,18 @@ namespace osu.Game.Online.Chat
|
||||
[Resolved]
|
||||
private UserLookupCache users { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private MultiplayerClient multiplayerClient { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private Storage storage { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private INotificationOverlay notifications { get; set; }
|
||||
|
||||
private readonly IBindable<APIUser> localUser = new Bindable<APIUser>();
|
||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||
private readonly IBindableList<APIRelation> localUserBlocks = new BindableList<APIRelation>();
|
||||
@@ -264,11 +287,11 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
switch (command.ToLowerInvariant())
|
||||
{
|
||||
case "np":
|
||||
case @"np":
|
||||
AddInternal(new NowPlayingCommand(target));
|
||||
break;
|
||||
|
||||
case "me":
|
||||
case @"me":
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
target.AddNewMessages(new ErrorMessage("Usage: /me [action]"));
|
||||
@@ -278,7 +301,7 @@ namespace osu.Game.Online.Chat
|
||||
PostMessage(content, true, target);
|
||||
break;
|
||||
|
||||
case "join":
|
||||
case @"join":
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
target.AddNewMessages(new ErrorMessage("Usage: /join [channel]"));
|
||||
@@ -296,9 +319,9 @@ namespace osu.Game.Online.Chat
|
||||
JoinChannel(channel);
|
||||
break;
|
||||
|
||||
case "chat":
|
||||
case "msg":
|
||||
case "query":
|
||||
case @"chat":
|
||||
case @"msg":
|
||||
case @"query":
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
target.AddNewMessages(new ErrorMessage($"Usage: /{command} [user]"));
|
||||
@@ -323,8 +346,72 @@ namespace osu.Game.Online.Chat
|
||||
api.Queue(request);
|
||||
break;
|
||||
|
||||
case "help":
|
||||
target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel], /chat [user], /np"));
|
||||
case @"roll":
|
||||
if (target.Type != ChannelType.Multiplayer || multiplayerClient?.Room?.ChannelID != target.Id)
|
||||
{
|
||||
target.AddNewMessages(new ErrorMessage("Cannot roll when not in a multiplayer room."));
|
||||
break;
|
||||
}
|
||||
|
||||
uint max = 100;
|
||||
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
{
|
||||
if (!uint.TryParse(content, out max) || max < 2 || max > 100)
|
||||
{
|
||||
target.AddNewMessages(new ErrorMessage("Usage: /roll [2-100]"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var rollRequest = new RollRequest { Max = max };
|
||||
multiplayerClient.SendMatchRequest(rollRequest).FireAndForget(onError: ex =>
|
||||
{
|
||||
string message = ex is HubException
|
||||
? $"Failed to roll: {ex.Message}"
|
||||
: "Failed to roll.";
|
||||
target.AddNewMessages(new ErrorMessage(message));
|
||||
});
|
||||
break;
|
||||
|
||||
case @"savelog":
|
||||
ProgressNotification notification = new ProgressNotification
|
||||
{
|
||||
State = ProgressNotificationState.Active,
|
||||
Text = NotificationsStrings.LogsExportOngoing,
|
||||
};
|
||||
notifications?.Post(notification);
|
||||
|
||||
exportChannelLog(target).ContinueWith(t =>
|
||||
{
|
||||
if (t.Exception != null)
|
||||
{
|
||||
Logger.Log($@"Failed to export channel log: {t.Exception}");
|
||||
notification.State = ProgressNotificationState.Cancelled;
|
||||
return;
|
||||
}
|
||||
|
||||
string result = t.GetResultSafely();
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
Logger.Log("Failed to export channel log due to missing storage.");
|
||||
notification.State = ProgressNotificationState.Cancelled;
|
||||
return;
|
||||
}
|
||||
|
||||
notification.CompletionText = NotificationsStrings.FileExportFinished(result);
|
||||
notification.CompletionClickAction = () =>
|
||||
{
|
||||
(storage as OsuStorage)?.GetExportStorage().PresentFileExternally(result);
|
||||
return true;
|
||||
};
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
});
|
||||
break;
|
||||
|
||||
case @"help":
|
||||
target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel], /chat [user], /np, /savelog, /roll [2-100] (multiplayer only)"));
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -679,6 +766,26 @@ namespace osu.Game.Online.Chat
|
||||
}
|
||||
}
|
||||
|
||||
[ItemCanBeNull]
|
||||
private async Task<string> exportChannelLog(Channel channel)
|
||||
{
|
||||
if (storage is not OsuStorage osuStorage)
|
||||
return null;
|
||||
|
||||
string filename = string.Format($@"chat-{channel.Name}-{DateTimeOffset.Now:yyyyMMdd-hhmmss}.txt").GetValidFilename();
|
||||
var exportStorage = osuStorage.GetExportStorage();
|
||||
|
||||
using (var file = exportStorage.CreateFileSafely(filename))
|
||||
{
|
||||
using var textWriter = new StreamWriter(file);
|
||||
|
||||
foreach (var message in channel.Messages)
|
||||
await textWriter.WriteLineAsync($@"{message.Timestamp:yyyy-MM-dd HH:mm} {message.Sender.Username}: {message.Content}").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
@@ -25,6 +25,16 @@ namespace osu.Game.Online
|
||||
{
|
||||
public partial class FriendPresenceNotifier : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum time between subsequent online/offline notifications.
|
||||
/// </summary>
|
||||
public double NotificationDebounceTime { get; set; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum time after a user has gone offline, before they're added to the offline alert queue.
|
||||
/// </summary>
|
||||
public double OfflineDebounceTime { get; set; } = 15000;
|
||||
|
||||
[Resolved]
|
||||
private INotificationOverlay notifications { get; set; } = null!;
|
||||
|
||||
@@ -42,13 +52,31 @@ namespace osu.Game.Online
|
||||
private readonly IBindableList<APIRelation> friends = new BindableList<APIRelation>();
|
||||
private readonly IBindableDictionary<int, UserPresence> friendPresences = new BindableDictionary<int, UserPresence>();
|
||||
|
||||
/// <summary>
|
||||
/// List of users that will be notified as having come online with the next notification.
|
||||
/// </summary>
|
||||
private readonly HashSet<APIUser> onlineAlertQueue = new HashSet<APIUser>();
|
||||
|
||||
/// <summary>
|
||||
/// List of users that will be notified as having gone offline with the next notification.
|
||||
/// </summary>
|
||||
private readonly HashSet<APIUser> offlineAlertQueue = new HashSet<APIUser>();
|
||||
|
||||
private double? nextOnlineAlertTime;
|
||||
private double? nextOfflineAlertTime;
|
||||
/// <summary>
|
||||
/// List of users that have gone offline, but we're waiting for them to potentially come online again before queueing them for notification.
|
||||
/// For example, if a user is quickly toggling between the "Online" and "Appear Offline" states.
|
||||
/// </summary>
|
||||
private readonly HashSet<APIUser> pendingOfflineUsers = new HashSet<APIUser>();
|
||||
|
||||
private const double debounce_time_before_notification = 1000;
|
||||
/// <summary>
|
||||
/// The post time for the next online notification.
|
||||
/// </summary>
|
||||
private double? nextOnlineAlertTime;
|
||||
|
||||
/// <summary>
|
||||
/// The post time for the next offline notification.
|
||||
/// </summary>
|
||||
private double? nextOfflineAlertTime;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
@@ -133,28 +161,55 @@ namespace osu.Game.Online
|
||||
APIRelation? friend = friends.FirstOrDefault(f => f.TargetID == friendId);
|
||||
|
||||
if (friend?.TargetUser is APIUser user)
|
||||
markUserOffline(user);
|
||||
markUserOfflineDebounced(user);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately registers a user for the next online notification alert.
|
||||
/// </summary>
|
||||
private void markUserOnline(APIUser user)
|
||||
{
|
||||
if (pendingOfflineUsers.Remove(user))
|
||||
return;
|
||||
|
||||
if (!offlineAlertQueue.Remove(user))
|
||||
{
|
||||
onlineAlertQueue.Add(user);
|
||||
nextOnlineAlertTime ??= Time.Current + debounce_time_before_notification;
|
||||
nextOnlineAlertTime ??= Time.Current + NotificationDebounceTime;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits <see cref="OfflineDebounceTime"/> before adding a user to the next offline notification alert.
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
private void markUserOfflineDebounced(APIUser user)
|
||||
{
|
||||
pendingOfflineUsers.Add(user);
|
||||
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
// Check if the friend has come back online.
|
||||
if (!pendingOfflineUsers.Remove(user))
|
||||
return;
|
||||
|
||||
markUserOffline(user);
|
||||
}, OfflineDebounceTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately registers a user for the next offline notification alert.
|
||||
/// </summary>
|
||||
private void markUserOffline(APIUser user)
|
||||
{
|
||||
if (!onlineAlertQueue.Remove(user))
|
||||
{
|
||||
offlineAlertQueue.Add(user);
|
||||
nextOfflineAlertTime ??= Time.Current + debounce_time_before_notification;
|
||||
nextOfflineAlertTime ??= Time.Current + NotificationDebounceTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ namespace osu.Game.Online.Metadata
|
||||
public override Task<BeatmapUpdates> GetChangesSince(int queueId)
|
||||
{
|
||||
if (connector?.IsConnected.Value != true)
|
||||
return Task.FromCanceled<BeatmapUpdates>(default);
|
||||
return Task.FromCanceled<BeatmapUpdates>(CancellationToken.None);
|
||||
|
||||
Logger.Log($"Requesting any changes since last known queue id {queueId}");
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace osu.Game.Online.Notifications.WebSocket
|
||||
|
||||
public Func<SocketMessage, bool>? HandleMessage;
|
||||
|
||||
public Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default)
|
||||
public Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = null)
|
||||
{
|
||||
if (HandleMessage?.Invoke(message) != true)
|
||||
throw new InvalidOperationException($@"{nameof(DummyNotificationsClient)} cannot process this message.");
|
||||
|
||||
@@ -26,6 +26,6 @@ namespace osu.Game.Online.Notifications.WebSocket
|
||||
/// <summary>
|
||||
/// Sends a <see cref="SocketMessage"/> to the notification server.
|
||||
/// </summary>
|
||||
Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default);
|
||||
Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace osu.Game.Online.Notifications.WebSocket
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default)
|
||||
public async Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = null)
|
||||
{
|
||||
if (socket.State != WebSocketState.Open)
|
||||
return;
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace osu.Game.Online.Notifications.WebSocket
|
||||
return client;
|
||||
}
|
||||
|
||||
public Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default)
|
||||
public Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = null)
|
||||
{
|
||||
if (CurrentConnection is not WebSocketNotificationsClient webSocketClient)
|
||||
return Task.CompletedTask;
|
||||
|
||||
@@ -173,7 +173,7 @@ namespace osu.Game.Online
|
||||
|
||||
// make sure a disconnect wasn't triggered (and this is still the active connection).
|
||||
if (!hasBeenCancelled)
|
||||
await Task.Run(connect, default).ConfigureAwait(false);
|
||||
await Task.Run(connect, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected Task Disconnect() => disconnect(true);
|
||||
|
||||
+1
-26
@@ -36,7 +36,6 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@@ -193,7 +192,7 @@ namespace osu.Game
|
||||
|
||||
protected readonly Bindable<LocalUserPlayingState> UserPlayingState = new Bindable<LocalUserPlayingState>();
|
||||
|
||||
protected OsuScreenStack ScreenStack;
|
||||
public OsuScreenStack ScreenStack { get; private set; }
|
||||
|
||||
protected BackButton BackButton => screenStackFooter.BackButton;
|
||||
protected ScreenFooter ScreenFooter => screenStackFooter.Footer;
|
||||
@@ -1053,30 +1052,6 @@ namespace osu.Game
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
var languages = Enum.GetValues<Language>();
|
||||
|
||||
var mappings = languages.Select(language =>
|
||||
{
|
||||
#if DEBUG
|
||||
if (language == Language.debug)
|
||||
return new LocaleMapping("debug", new DebugLocalisationStore());
|
||||
#endif
|
||||
|
||||
string cultureCode = language.ToCultureCode();
|
||||
|
||||
try
|
||||
{
|
||||
return new LocaleMapping(new ResourceManagerLocalisationStore(cultureCode));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, $"Could not load localisations for language \"{cultureCode}\"");
|
||||
return null;
|
||||
}
|
||||
}).Where(m => m != null);
|
||||
|
||||
Localisation.AddLocaleMappings(mappings);
|
||||
|
||||
// The next time this is updated is in UpdateAfterChildren, which occurs too late and results
|
||||
// in the cursor being shown for a few frames during the intro.
|
||||
// This prevents the cursor from showing until we have a screen with CursorVisible = true
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
@@ -305,6 +306,7 @@ namespace osu.Game
|
||||
|
||||
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteUrl;
|
||||
|
||||
// Initialise localisation
|
||||
frameworkLocale = frameworkConfig.GetBindable<string>(FrameworkSetting.Locale);
|
||||
frameworkLocale.BindValueChanged(_ => updateLanguage());
|
||||
|
||||
@@ -500,6 +502,33 @@ namespace osu.Game
|
||||
Fonts.AddStore(new OsuIcon.OsuIconStore(Textures));
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
var localeMappings = Enum.GetValues<Language>().Select(language =>
|
||||
{
|
||||
#if DEBUG
|
||||
if (language == Language.debug)
|
||||
return new LocaleMapping("debug", new DebugLocalisationStore());
|
||||
#endif
|
||||
|
||||
string cultureCode = language.ToCultureCode();
|
||||
|
||||
try
|
||||
{
|
||||
return new LocaleMapping(new ResourceManagerLocalisationStore(cultureCode));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, $"Could not load localisations for language \"{cultureCode}\"");
|
||||
return null;
|
||||
}
|
||||
}).Where(m => m != null);
|
||||
|
||||
Localisation.AddLocaleMappings(localeMappings);
|
||||
}
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
|
||||
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
@@ -517,6 +546,8 @@ namespace osu.Game
|
||||
host.ExceptionThrown += onExceptionThrown;
|
||||
}
|
||||
|
||||
#region Exit handling
|
||||
|
||||
/// <summary>
|
||||
/// Use to programatically exit the game as if the user was triggering via alt-f4.
|
||||
/// By default, will keep persisting until an exit occurs (exit may be blocked multiple times).
|
||||
@@ -530,12 +561,28 @@ namespace osu.Game
|
||||
Scheduler.AddDelayed(AttemptExit, 2000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An action that restarts the application after it has exited.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public Action RestartOnExitAction { private get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Signals that the application should not be restarted after it is exited.
|
||||
/// </summary>
|
||||
public void CancelRestartOnExit()
|
||||
{
|
||||
RestartOnExitAction = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If supported by the platform, the game will automatically restart after the next exit.
|
||||
/// </summary>
|
||||
/// <returns>Whether a restart operation was queued.</returns>
|
||||
public virtual bool RestartAppWhenExited() => false;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Perform migration of user data to a specified path.
|
||||
/// </summary>
|
||||
@@ -742,6 +789,8 @@ namespace osu.Game
|
||||
|
||||
if (Host != null)
|
||||
Host.ExceptionThrown -= onExceptionThrown;
|
||||
|
||||
RestartOnExitAction?.Invoke();
|
||||
}
|
||||
|
||||
ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.BeatmapLoaded ? Beatmap.Value.Beatmap.ControlPointInfo : null;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Overlays.Dashboard.Friends;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
@@ -15,6 +18,17 @@ namespace osu.Game.Overlays.Dashboard.CurrentlyOnline
|
||||
{
|
||||
public partial class CurrentlyOnlineDisplay : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// The current state of the <see cref="DashboardOverlay"/>.
|
||||
/// Presence is only updated when this value is <see cref="Visibility.Visible"/>.
|
||||
/// </summary>
|
||||
public readonly Bindable<Visibility> OverlayState = new Bindable<Visibility>(Visibility.Visible);
|
||||
|
||||
[Resolved]
|
||||
private MetadataClient metadataClient { get; set; } = null!;
|
||||
|
||||
private readonly IBindable<bool> isConnected = new Bindable<bool>();
|
||||
|
||||
private Box background = null!;
|
||||
private UserListToolbar userListToolbar = null!;
|
||||
private Container<RealtimeUserList> listContainer = null!;
|
||||
@@ -22,6 +36,7 @@ namespace osu.Game.Overlays.Dashboard.CurrentlyOnline
|
||||
private BasicSearchTextBox searchTextBox = null!;
|
||||
|
||||
private CancellationTokenSource? listLoadCancellation;
|
||||
private IDisposable? userPresenceWatchToken;
|
||||
|
||||
public CurrentlyOnlineDisplay()
|
||||
{
|
||||
@@ -114,6 +129,11 @@ namespace osu.Game.Overlays.Dashboard.CurrentlyOnline
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
isConnected.BindTo(metadataClient.IsConnected);
|
||||
isConnected.BindValueChanged(_ => updateUserPresenceState());
|
||||
|
||||
OverlayState.BindValueChanged(_ => updateUserPresenceState(), true);
|
||||
|
||||
userListToolbar.DisplayStyle.BindValueChanged(_ => reloadList(), true);
|
||||
}
|
||||
|
||||
@@ -147,12 +167,28 @@ namespace osu.Game.Overlays.Dashboard.CurrentlyOnline
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUserPresenceState()
|
||||
{
|
||||
if (!isConnected.Value)
|
||||
return;
|
||||
|
||||
if (OverlayState.Value == Visibility.Visible)
|
||||
userPresenceWatchToken ??= metadataClient.BeginWatchingUserPresence();
|
||||
else
|
||||
{
|
||||
userPresenceWatchToken?.Dispose();
|
||||
userPresenceWatchToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
listLoadCancellation?.Cancel();
|
||||
listLoadCancellation?.Dispose();
|
||||
|
||||
userPresenceWatchToken?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Overlays.Dashboard;
|
||||
using osu.Game.Overlays.Dashboard.CurrentlyOnline;
|
||||
using osu.Game.Overlays.Dashboard.Friends;
|
||||
@@ -14,12 +10,6 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
public partial class DashboardOverlay : TabbableOnlineOverlay<DashboardOverlayHeader, DashboardOverlayTabs>
|
||||
{
|
||||
[Resolved]
|
||||
private MetadataClient metadataClient { get; set; } = null!;
|
||||
|
||||
private IBindable<bool> metadataConnected = null!;
|
||||
private IDisposable? userPresenceWatchToken;
|
||||
|
||||
public DashboardOverlay()
|
||||
: base(OverlayColourScheme.Purple)
|
||||
{
|
||||
@@ -38,36 +28,15 @@ namespace osu.Game.Overlays
|
||||
break;
|
||||
|
||||
case DashboardOverlayTabs.CurrentlyPlaying:
|
||||
LoadDisplay(new CurrentlyOnlineDisplay());
|
||||
LoadDisplay(new CurrentlyOnlineDisplay
|
||||
{
|
||||
OverlayState = { BindTarget = State }
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotImplementedException($"Display for {tab} tab is not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
metadataConnected = metadataClient.IsConnected.GetBoundCopy();
|
||||
metadataConnected.BindValueChanged(_ => updateUserPresenceState());
|
||||
State.BindValueChanged(_ => updateUserPresenceState());
|
||||
updateUserPresenceState();
|
||||
}
|
||||
|
||||
private void updateUserPresenceState()
|
||||
{
|
||||
if (!metadataConnected.Value)
|
||||
return;
|
||||
|
||||
if (State.Value == Visibility.Visible)
|
||||
userPresenceWatchToken ??= metadataClient.BeginWatchingUserPresence();
|
||||
else
|
||||
{
|
||||
userPresenceWatchToken?.Dispose();
|
||||
userPresenceWatchToken = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +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 osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Music
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="CollectionDropdown"/> for use in the <see cref="NowPlayingOverlay"/>.
|
||||
/// </summary>
|
||||
public partial class NowPlayingCollectionDropdown : CollectionDropdown // TODO: class is now unused. if we decide this isn't coming back it can be nuked.
|
||||
{
|
||||
protected override bool ShowManageCollectionsItem => false;
|
||||
|
||||
protected override CollectionDropdownHeader CreateCollectionHeader() => new CollectionsHeader();
|
||||
|
||||
protected override CollectionDropdownMenu CreateCollectionMenu() => new CollectionsMenu();
|
||||
|
||||
private partial class CollectionsMenu : CollectionDropdownMenu
|
||||
{
|
||||
public CollectionsMenu()
|
||||
{
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
BackgroundColour = colours.Gray4;
|
||||
SelectionColour = colours.Gray5;
|
||||
HoverColour = colours.Gray6;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class CollectionsHeader : CollectionDropdownHeader
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
BackgroundColour = colours.Gray4;
|
||||
BackgroundColourHover = colours.Gray6;
|
||||
}
|
||||
|
||||
public CollectionsHeader()
|
||||
{
|
||||
CornerRadius = 5;
|
||||
Height = 30;
|
||||
Chevron.Size = new Vector2(14);
|
||||
Chevron.Margin = new MarginPadding(0);
|
||||
Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 10, Right = 10 };
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.3f),
|
||||
Radius = 3,
|
||||
Offset = new Vector2(0f, 1f),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,9 @@ namespace osu.Game.Overlays
|
||||
/// </summary>
|
||||
private const double restart_cutoff_point = 5000;
|
||||
|
||||
public const double TRACK_FADE_IN_TIME = 250;
|
||||
public const double TRACK_FADE_OUT_TIME = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user has requested the track to be paused. Use <see cref="IsPlaying"/> to determine whether the track is still playing.
|
||||
/// </summary>
|
||||
@@ -518,25 +521,20 @@ namespace osu.Game.Overlays
|
||||
|
||||
CurrentTrack = queuedTrack;
|
||||
|
||||
// At this point we may potentially be in an async context from tests. This is extremely dangerous but we have to make do for now.
|
||||
// CurrentTrack is immediately updated above for situations where a immediate knowledge about the new track is required,
|
||||
// but the mutation of the hierarchy is scheduled to avoid exceptions.
|
||||
Schedule(() =>
|
||||
{
|
||||
lastTrack.VolumeTo(0, 500, Easing.Out).Expire();
|
||||
lastTrack.VolumeTo(0, TRACK_FADE_OUT_TIME, Easing.Out).Expire();
|
||||
|
||||
if (queuedTrack == CurrentTrack)
|
||||
{
|
||||
AddInternal(queuedTrack);
|
||||
queuedTrack.VolumeTo(0).Then().VolumeTo(1, 300, Easing.Out);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the track has changed since the call to changeTrack, it is safe to dispose the
|
||||
// queued track rather than consume it.
|
||||
queuedTrack.Dispose();
|
||||
}
|
||||
});
|
||||
if (queuedTrack == CurrentTrack)
|
||||
{
|
||||
queuedTrack.Volume.Value = 0;
|
||||
AddInternal(queuedTrack);
|
||||
queuedTrack.Delay(50).VolumeTo(1, TRACK_FADE_IN_TIME, Easing.Out);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the track has changed since the call to changeTrack, it is safe to dispose the
|
||||
// queued track rather than consume it.
|
||||
queuedTrack.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private DrawableTrack getQueuedTrack()
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
private const float player_width = 400;
|
||||
private const float player_height = 130;
|
||||
private const float transition_length = 800;
|
||||
private const float transition_length = 500;
|
||||
private const float progress_height = 10;
|
||||
private const float bottom_black_area_height = 55;
|
||||
private const float margin = 10;
|
||||
@@ -289,7 +289,7 @@ namespace osu.Game.Overlays
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(transition_length, Easing.OutQuint);
|
||||
dragContainer.ScaleTo(1, transition_length, Easing.OutElastic);
|
||||
dragContainer.ScaleTo(1, transition_length, Easing.OutElasticHalf);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace osu.Game.Overlays
|
||||
Button.State = Target > button_scroll_position || lastScrollTarget.Value != null ? Visibility.Visible : Visibility.Hidden;
|
||||
}
|
||||
|
||||
protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = default)
|
||||
protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = null)
|
||||
{
|
||||
base.OnUserScroll(value, animated, distanceDecay);
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
var bindings = realm.All<RealmKeyBinding>()
|
||||
.Where(b => b.RulesetName == null && b.Variant == null)
|
||||
.Detach();
|
||||
.AsEnumerable().Detach();
|
||||
|
||||
var actionsInSection = GlobalActionContainer.GetGlobalActionsFor(category).Cast<int>().ToHashSet();
|
||||
return bindings.Where(kb => actionsInSection.Contains(kb.ActionInt));
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
/// <summary>
|
||||
/// Based on ultrawide monitor configurations, plus a bit of lenience for users which are intentionally aiming for higher horizontal velocity.
|
||||
/// </summary>
|
||||
private const float largest_feasible_aspect_ratio = 23f / 9;
|
||||
private const float largest_feasible_aspect_ratio = 32f / 9;
|
||||
|
||||
private readonly BindableNumber<float> aspectRatio = new BindableFloat(1)
|
||||
{
|
||||
|
||||
@@ -67,13 +67,13 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing
|
||||
public DifficultyHitObject Previous(int backwardsIndex)
|
||||
{
|
||||
int index = Index - (backwardsIndex + 1);
|
||||
return index >= 0 && index < difficultyHitObjects.Count ? difficultyHitObjects[index] : default;
|
||||
return index >= 0 && index < difficultyHitObjects.Count ? difficultyHitObjects[index] : null;
|
||||
}
|
||||
|
||||
public DifficultyHitObject Next(int forwardsIndex)
|
||||
{
|
||||
int index = Index + (forwardsIndex + 1);
|
||||
return index >= 0 && index < difficultyHitObjects.Count ? difficultyHitObjects[index] : default;
|
||||
return index >= 0 && index < difficultyHitObjects.Count ? difficultyHitObjects[index] : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
}
|
||||
}
|
||||
|
||||
public DifficultyBindableWithCurrent(float? defaultValue = default)
|
||||
public DifficultyBindableWithCurrent(float? defaultValue = null)
|
||||
: base(defaultValue)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Mods
|
||||
public BindableNumber<double> MinimumAccuracy { get; } = new BindableDouble
|
||||
{
|
||||
MinValue = 0.60,
|
||||
MaxValue = 0.99,
|
||||
Precision = 0.01,
|
||||
MaxValue = 0.999,
|
||||
Precision = 0.001,
|
||||
Default = 0.9,
|
||||
Value = 0.9,
|
||||
};
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
player.DimmableStoryboard.IgnoreUserSettings.Value = true;
|
||||
|
||||
player.BreakOverlay.Hide();
|
||||
player.OverlayComponents.Hide();
|
||||
}
|
||||
|
||||
public bool PerformFail() => false;
|
||||
|
||||
@@ -69,11 +69,9 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
|
||||
HitObject hitObject = entry.HitObject;
|
||||
|
||||
if (!entryMap.ContainsKey(hitObject))
|
||||
if (!entryMap.Remove(hitObject))
|
||||
throw new InvalidOperationException($@"The {nameof(HitObjectLifetimeEntry)} is not contained in this {nameof(HitObjectEntryManager)}.");
|
||||
|
||||
entryMap.Remove(hitObject);
|
||||
|
||||
// If the entry has a parent, unset it and remove the entry from the parents' children.
|
||||
if (parentMap.Remove(entry, out var parent) && entryMap.TryGetValue(parent, out var parentEntry))
|
||||
parentEntry.NestedEntries.Remove(entry);
|
||||
|
||||
@@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
applyRulesetMods(Mods, config);
|
||||
|
||||
loadObjects(cancellationToken ?? default);
|
||||
loadObjects(cancellationToken ?? CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
/// </summary>
|
||||
public readonly double Position;
|
||||
|
||||
public PositionMapping(double time, MultiplierControlPoint controlPoint = null, double position = default)
|
||||
public PositionMapping(double time, MultiplierControlPoint controlPoint = null, double position = 0)
|
||||
{
|
||||
Time = time;
|
||||
ControlPoint = controlPoint;
|
||||
|
||||
@@ -10,18 +10,28 @@ namespace osu.Game.Screens.Backgrounds
|
||||
{
|
||||
public partial class BackgroundScreenBlack : BackgroundScreen
|
||||
{
|
||||
public BackgroundScreenBlack()
|
||||
private readonly double delayBeforeBlack;
|
||||
private readonly Box box;
|
||||
|
||||
public BackgroundScreenBlack(double delayBeforeBlack = 0)
|
||||
{
|
||||
InternalChild = new Box
|
||||
this.delayBeforeBlack = delayBeforeBlack;
|
||||
|
||||
InternalChild = box = new Box
|
||||
{
|
||||
Colour = Color4.Black,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
Alpha = 0;
|
||||
}
|
||||
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
Show();
|
||||
this
|
||||
.Delay(delayBeforeBlack)
|
||||
.FadeIn(200)
|
||||
.OnComplete(_ => box.Hide());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Storyboards.Drawables;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Backgrounds
|
||||
{
|
||||
public partial class EditorBackgroundScreen : BackgroundScreen
|
||||
{
|
||||
private readonly EditorBeatmap editorBeatmap;
|
||||
private readonly Container dimContainer;
|
||||
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
private Bindable<float> dimLevel = null!;
|
||||
private Bindable<bool> showStoryboard = null!;
|
||||
|
||||
private BeatmapBackground background = null!;
|
||||
private Container storyboardContainer = null!;
|
||||
private BeatmapBackgroundWithStoryboard? background;
|
||||
|
||||
private IFrameBasedClock? clockSource;
|
||||
private readonly Container content;
|
||||
private readonly Box blackBox;
|
||||
|
||||
// We retrieve IBindable<WorkingBeatmap> from our dependency cache instead of passing WorkingBeatmap directly into EditorBackgroundScreen.
|
||||
// Otherwise, DummyWorkingBeatmap will be erroneously passed in whenever creating a new beatmap (since the Schedule() in the Editor that populates
|
||||
@@ -40,40 +38,35 @@ namespace osu.Game.Screens.Backgrounds
|
||||
|
||||
public EditorBackgroundScreen(EditorBeatmap editorBeatmap)
|
||||
{
|
||||
this.editorBeatmap = editorBeatmap;
|
||||
InternalChild = dimContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
// This adds overdraw but makes transitions not suck.
|
||||
// There's probably a better way to do this, but it's high effort.
|
||||
blackBox = new Box
|
||||
{
|
||||
Colour = Color4.Black,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
content = new EditorSkinProvidingContainer(editorBeatmap)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
dimContainer.AddRange(createContent());
|
||||
background = dimContainer.OfType<BeatmapBackground>().Single();
|
||||
storyboardContainer = dimContainer.OfType<Container>().Single();
|
||||
|
||||
dimLevel = config.GetBindable<float>(OsuSetting.EditorDim);
|
||||
showStoryboard = config.GetBindable<bool>(OsuSetting.EditorShowStoryboard);
|
||||
}
|
||||
|
||||
private IEnumerable<Drawable> createContent() =>
|
||||
[
|
||||
new BeatmapBackground(beatmap.Value) { RelativeSizeAxes = Axes.Both, },
|
||||
// one reason for this kooky container nesting being here is that the storyboard needs a custom clock
|
||||
// but also needs it on an isolated-enough level that doesn't break screen stack expiry logic (which happens if the clock was put on `this`),
|
||||
// or doesn't make it literally impossible to fade the storyboard in/out in real time (which happens if the fade transforms were to be applied directly to the storyboard).
|
||||
// another is that we need `EditorSkinProvidingContainer` so that storyboard sample lookups succeed.
|
||||
new EditorSkinProvidingContainer(editorBeatmap)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new DrawableStoryboard(beatmap.Value.Storyboard)
|
||||
{
|
||||
Clock = clockSource ?? Clock,
|
||||
}
|
||||
}
|
||||
];
|
||||
content.Child = createContent();
|
||||
updateState(withAnimation: false);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
@@ -81,38 +74,45 @@ namespace osu.Game.Screens.Backgrounds
|
||||
|
||||
dimLevel.BindValueChanged(_ => dimContainer.FadeColour(OsuColour.Gray(1 - dimLevel.Value), 500, Easing.OutQuint), true);
|
||||
showStoryboard.BindValueChanged(_ => updateState());
|
||||
updateState(0);
|
||||
|
||||
updateState(withAnimation: false);
|
||||
}
|
||||
|
||||
private void updateState(double duration = 500)
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
storyboardContainer.FadeTo(showStoryboard.Value ? 1 : 0, duration, Easing.OutQuint);
|
||||
// yes, this causes overdraw, but is also a (crude) fix for bad-looking transitions on screen entry
|
||||
// caused by the previous background on the background stack poking out from under this one and then instantly fading out
|
||||
background.FadeColour(beatmap.Value.Storyboard.ReplacesBackground && showStoryboard.Value ? Colour4.Black : Colour4.White, duration, Easing.OutQuint);
|
||||
base.OnEntering(e);
|
||||
blackBox.LifetimeEnd = LatestTransformEndTime;
|
||||
}
|
||||
|
||||
public void ChangeClockSource(IFrameBasedClock frameBasedClock)
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
clockSource = frameBasedClock;
|
||||
if (IsLoaded)
|
||||
storyboardContainer.Child.Clock = frameBasedClock;
|
||||
// The storyboard will do weird things with clock time changing on exit, so let's just hide it instead.
|
||||
background?.UnloadStoryboard();
|
||||
|
||||
return base.OnExiting(e);
|
||||
}
|
||||
|
||||
public void RefreshBackground()
|
||||
public void RefreshBackgroundAsync()
|
||||
{
|
||||
cancellationTokenSource?.Cancel();
|
||||
LoadComponentsAsync(createContent(), loaded =>
|
||||
LoadComponentAsync(createContent(), loaded =>
|
||||
{
|
||||
dimContainer.Clear();
|
||||
dimContainer.AddRange(loaded);
|
||||
|
||||
background = dimContainer.OfType<BeatmapBackground>().Single();
|
||||
storyboardContainer = dimContainer.OfType<Container>().Single();
|
||||
updateState(0);
|
||||
content.Child = loaded;
|
||||
updateState(withAnimation: false);
|
||||
}, (cancellationTokenSource = new CancellationTokenSource()).Token);
|
||||
}
|
||||
|
||||
private Drawable createContent() => background = new BeatmapBackgroundWithStoryboard(beatmap.Value)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
StoryboardLoaded = () => updateState(withAnimation: false)
|
||||
};
|
||||
|
||||
private void updateState(bool withAnimation = true)
|
||||
{
|
||||
background?.Storyboard.FadeTo(showStoryboard.Value ? 1 : 0, withAnimation ? 500 : 0, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public override bool Equals(BackgroundScreen? other)
|
||||
{
|
||||
if (other is not EditorBackgroundScreen otherBeatmapBackground)
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
|
||||
|
||||
case EffectControlPoint:
|
||||
if (!showScrollSpeed)
|
||||
return;
|
||||
continue;
|
||||
|
||||
AddInternal(new ControlPointVisualisation(point)
|
||||
{
|
||||
|
||||
@@ -472,8 +472,6 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
|
||||
editorBackgroundDim.BindValueChanged(_ => setUpBackground());
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
@@ -845,26 +843,15 @@ namespace osu.Game.Screens.Edit
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnEntering(e);
|
||||
setUpBackground();
|
||||
setUpTrack(seekToStart: true);
|
||||
}
|
||||
|
||||
public override void OnResuming(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnResuming(e);
|
||||
setUpBackground();
|
||||
setUpTrack();
|
||||
}
|
||||
|
||||
private void setUpBackground()
|
||||
{
|
||||
ApplyToBackground(b =>
|
||||
{
|
||||
var editorBackground = (EditorBackgroundScreen)b;
|
||||
editorBackground.ChangeClockSource(clock);
|
||||
});
|
||||
}
|
||||
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
currentScreen?.OnExiting(e);
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
(metadata, name) => metadata.BackgroundFile = name);
|
||||
|
||||
headerBackground.UpdateBackground();
|
||||
editor?.ApplyToBackground(bg => ((EditorBackgroundScreen)bg).RefreshBackground());
|
||||
editor?.ApplyToBackground(bg => ((EditorBackgroundScreen)bg).RefreshBackgroundAsync());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,13 @@ namespace osu.Game.Screens.Menu
|
||||
if (notifications.HasOngoingOperations)
|
||||
{
|
||||
var ongoingOperations = notifications.OngoingOperations.ToArray();
|
||||
string ongoingOperationsText = ongoingOperations.Take(10).Aggregate(string.Empty, (current, n) => current + $"{n.Text} ({n.Progress:0%})\n");
|
||||
string ongoingOperationsText = ongoingOperations.Take(10).Aggregate(string.Empty, (current, n) =>
|
||||
{
|
||||
if (n.Progress > 0)
|
||||
return current + $"{n.Text} ({n.Progress:0%})\n";
|
||||
|
||||
return current + $"{n.Text}\n";
|
||||
});
|
||||
|
||||
LocalisableString ongoingOperationsLocalisableString;
|
||||
|
||||
|
||||
@@ -429,6 +429,7 @@ namespace osu.Game.Screens.Menu
|
||||
}, () =>
|
||||
{
|
||||
holdToExitGameOverlay.Abort();
|
||||
Game.CancelRestartOnExit();
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,88 +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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// A header used in the multiplayer interface which shows text / details beneath a line.
|
||||
/// </summary>
|
||||
public partial class OverlinedHeader : CompositeDrawable
|
||||
{
|
||||
private bool showLine = true;
|
||||
|
||||
public bool ShowLine
|
||||
{
|
||||
get => showLine;
|
||||
set
|
||||
{
|
||||
showLine = value;
|
||||
line.Alpha = value ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public Bindable<string> Details = new Bindable<string>();
|
||||
|
||||
private readonly Circle line;
|
||||
private readonly OsuSpriteText details;
|
||||
|
||||
public OverlinedHeader(LocalisableString title)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Margin = new MarginPadding { Bottom = 5 };
|
||||
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
line = new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 2,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(10, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = title,
|
||||
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold)
|
||||
},
|
||||
details = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold)
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Details.BindValueChanged(val => details.Text = val.NewValue);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
line.Colour = colours.Yellow;
|
||||
details.Colour = colours.Yellow;
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -3,19 +3,20 @@
|
||||
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Components
|
||||
{
|
||||
public partial class OverlinedPlaylistHeader : OverlinedHeader
|
||||
public partial class PlaylistHeader : SectionHeader
|
||||
{
|
||||
private readonly Room room;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
public OverlinedPlaylistHeader(Room room)
|
||||
public PlaylistHeader(Room room)
|
||||
: base("Playlist")
|
||||
{
|
||||
this.room = room;
|
||||
@@ -36,7 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
||||
}
|
||||
|
||||
private void updateDuration()
|
||||
=> Details.Value = room.Playlist.GetTotalDuration(rulesets);
|
||||
=> DetailsText.Value = $"{room.Playlist.GetTotalDuration(rulesets)}";
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
@@ -252,7 +252,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new SectionHeader("Chat")
|
||||
new SectionHeader(OnlinePlayStrings.Chat)
|
||||
},
|
||||
[new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both }]
|
||||
},
|
||||
|
||||
@@ -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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
@@ -10,13 +12,18 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
@@ -40,6 +47,21 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
[Resolved]
|
||||
private OverlayColourProvider overlayColours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<WorkingBeatmap> globalBeatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<RulesetInfo> globalRuleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<IReadOnlyList<Mod>> globalMods { get; set; } = null!;
|
||||
|
||||
private Container<RankedPlayCard> cardColumn = null!;
|
||||
private Drawable separator = null!;
|
||||
private Drawable detailsColumn = null!;
|
||||
@@ -128,6 +150,23 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
];
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
MultiplayerPlaylistItem item = Client.Room!.CurrentPlaylistItem;
|
||||
|
||||
RulesetInfo ruleset = rulesets.GetRuleset(item.RulesetID)!;
|
||||
Ruleset rulesetInstance = ruleset.CreateInstance();
|
||||
BeatmapInfo? localBeatmap = beatmapManager.QueryOnlineBeatmapId(item.BeatmapID);
|
||||
|
||||
globalBeatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
|
||||
globalRuleset.Value = ruleset;
|
||||
globalMods.Value = item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
|
||||
Client.ChangeState(MultiplayerUserState.Ready).FireAndForget();
|
||||
}
|
||||
|
||||
public override void OnEntering(RankedPlaySubScreen? previous)
|
||||
{
|
||||
base.OnEntering(previous);
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
// 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.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
public partial class RankedPlayBeatmapAvailabilityTracker : OnlinePlayBeatmapAvailabilityTracker
|
||||
{
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapModelDownloader beatmapDownloader { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
private CancellationTokenSource? downloadCheckCancellation;
|
||||
private int? lastDownloadCheckedBeatmapId;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Availability.BindValueChanged(onBeatmapAvailabilityChanged);
|
||||
|
||||
client.SettingsChanged += onSettingsChanged;
|
||||
onSettingsChanged(client.Room!.Settings);
|
||||
}
|
||||
|
||||
private void onSettingsChanged(MultiplayerRoomSettings settings)
|
||||
{
|
||||
PlaylistItem.Value = new PlaylistItem(client.Room!.CurrentPlaylistItem);
|
||||
checkForAutomaticDownload(client.Room!.CurrentPlaylistItem);
|
||||
}
|
||||
|
||||
private void onBeatmapAvailabilityChanged(ValueChangedEvent<BeatmapAvailability> availability)
|
||||
{
|
||||
client.ChangeBeatmapAvailability(availability.NewValue).FireAndForget();
|
||||
}
|
||||
|
||||
private void checkForAutomaticDownload(MultiplayerPlaylistItem item)
|
||||
{
|
||||
// This method is called every time anything changes in the room.
|
||||
// This could result in download requests firing far too often, when we only expect them to fire once per beatmap.
|
||||
//
|
||||
// Without this check, we would see especially egregious behaviour when a user has hit the download rate limit.
|
||||
if (lastDownloadCheckedBeatmapId == item.BeatmapID)
|
||||
return;
|
||||
|
||||
lastDownloadCheckedBeatmapId = item.BeatmapID;
|
||||
|
||||
downloadCheckCancellation?.Cancel();
|
||||
|
||||
if (beatmapManager.IsAvailableLocally(new APIBeatmap { OnlineID = item.BeatmapID }))
|
||||
return;
|
||||
|
||||
// In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes.
|
||||
// ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised.
|
||||
beatmapLookupCache
|
||||
.GetBeatmapAsync(item.BeatmapID, (downloadCheckCancellation = new CancellationTokenSource()).Token)
|
||||
.ContinueWith(resolved => Schedule(() =>
|
||||
{
|
||||
APIBeatmapSet? beatmapSet = resolved.GetResultSafely()?.BeatmapSet;
|
||||
|
||||
if (beatmapSet == null)
|
||||
return;
|
||||
|
||||
beatmapDownloader.Download(beatmapSet, config.Get<bool>(OsuSetting.PreferNoVideo));
|
||||
}));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
client.SettingsChanged -= onSettingsChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,10 @@
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
@@ -16,12 +14,8 @@ using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
|
||||
using osu.Game.Online.Rooms;
|
||||
@@ -57,36 +51,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
|
||||
public override float BackgroundParallaxAmount => 0;
|
||||
|
||||
[Cached(typeof(OnlinePlayBeatmapAvailabilityTracker))]
|
||||
private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new MultiplayerBeatmapAvailabilityTracker();
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapModelDownloader beatmapDownloader { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay dialogOverlay { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audio { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private PreviewTrackManager previewTrackManager { get; set; } = null!;
|
||||
|
||||
@@ -103,8 +79,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
private IBindable<RankedPlayStage> stage = null!;
|
||||
|
||||
private Sample? sampleStart;
|
||||
private CancellationTokenSource? downloadCheckCancellation;
|
||||
private int? lastDownloadCheckedBeatmapId;
|
||||
|
||||
private readonly Bindable<Visibility> cornerPieceVisibility = new Bindable<Visibility>();
|
||||
private readonly Bindable<bool> showBeatmapBackground = new Bindable<bool>();
|
||||
@@ -125,7 +99,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
matchInfo = new RankedPlayMatchInfo(),
|
||||
beatmapAvailabilityTracker,
|
||||
new RankedPlayBeatmapAvailabilityTracker(),
|
||||
new GlobalScrollAdjustsVolume(),
|
||||
new PopoverContainer
|
||||
{
|
||||
@@ -176,11 +150,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
client.UserStateChanged += onUserStateChanged;
|
||||
client.SettingsChanged += onSettingsChanged;
|
||||
client.LoadRequested += onLoadRequested;
|
||||
|
||||
beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true);
|
||||
|
||||
int localUserId = api.LocalUser.Value.OnlineID;
|
||||
int opponentUserId = ((RankedPlayRoomState)client.Room!.MatchState!).Users.Keys.Single(it => it != localUserId);
|
||||
|
||||
@@ -256,24 +227,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
this.MakeCurrent();
|
||||
}
|
||||
|
||||
private void onSettingsChanged(MultiplayerRoomSettings _) => Scheduler.Add(() =>
|
||||
{
|
||||
checkForAutomaticDownload();
|
||||
updateGameplayState();
|
||||
});
|
||||
|
||||
private void onLoadRequested() => Scheduler.Add(() =>
|
||||
{
|
||||
updateGameplayState();
|
||||
|
||||
if (Beatmap.IsDefault)
|
||||
{
|
||||
Logger.Log("Aborting gameplay start - beatmap not downloaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
sampleStart?.Play();
|
||||
|
||||
this.Push(new MultiplayerPlayerLoader(() => new ScreenGameplay(new Room(room), new PlaylistItem(client.Room!.CurrentPlaylistItem), room.Users.ToArray())));
|
||||
});
|
||||
|
||||
@@ -329,92 +285,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
}
|
||||
}
|
||||
|
||||
private void onBeatmapAvailabilityChanged(ValueChangedEvent<BeatmapAvailability> e) => Scheduler.Add(() =>
|
||||
{
|
||||
if (client.Room == null || client.LocalUser == null)
|
||||
return;
|
||||
|
||||
client.ChangeBeatmapAvailability(e.NewValue).FireAndForget();
|
||||
|
||||
switch (e.NewValue.State)
|
||||
{
|
||||
case DownloadState.NotDownloaded:
|
||||
case DownloadState.LocallyAvailable:
|
||||
updateGameplayState();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
private void updateGameplayState()
|
||||
{
|
||||
MultiplayerPlaylistItem item = client.Room!.CurrentPlaylistItem;
|
||||
|
||||
if (item.Expired)
|
||||
return;
|
||||
|
||||
RulesetInfo ruleset = rulesets.GetRuleset(item.RulesetID)!;
|
||||
Ruleset rulesetInstance = ruleset.CreateInstance();
|
||||
|
||||
// Update global gameplay state to correspond to the new selection.
|
||||
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
|
||||
var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", item.BeatmapID);
|
||||
|
||||
if (localBeatmap != null)
|
||||
{
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
|
||||
Ruleset.Value = ruleset;
|
||||
Mods.Value = item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
|
||||
// Notify the server that the beatmap has been set and that we are ready to start gameplay.
|
||||
if (client.LocalUser!.State == MultiplayerUserState.Idle)
|
||||
client.ChangeState(MultiplayerUserState.Ready).FireAndForget();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Notify the server that we don't have the beatmap.
|
||||
if (client.LocalUser!.State == MultiplayerUserState.Ready)
|
||||
client.ChangeState(MultiplayerUserState.Idle).FireAndForget();
|
||||
}
|
||||
|
||||
client.ChangeBeatmapAvailability(beatmapAvailabilityTracker.Availability.Value).FireAndForget();
|
||||
}
|
||||
|
||||
private void checkForAutomaticDownload()
|
||||
{
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem;
|
||||
|
||||
// This method is called every time anything changes in the room.
|
||||
// This could result in download requests firing far too often, when we only expect them to fire once per beatmap.
|
||||
//
|
||||
// Without this check, we would see especially egregious behaviour when a user has hit the download rate limit.
|
||||
if (lastDownloadCheckedBeatmapId == item.BeatmapID)
|
||||
return;
|
||||
|
||||
lastDownloadCheckedBeatmapId = item.BeatmapID;
|
||||
|
||||
downloadCheckCancellation?.Cancel();
|
||||
|
||||
if (beatmapManager.IsAvailableLocally(new APIBeatmap { OnlineID = item.BeatmapID }))
|
||||
return;
|
||||
|
||||
// In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes.
|
||||
// ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised.
|
||||
beatmapLookupCache
|
||||
.GetBeatmapAsync(item.BeatmapID, (downloadCheckCancellation = new CancellationTokenSource()).Token)
|
||||
.ContinueWith(resolved => Schedule(() =>
|
||||
{
|
||||
APIBeatmapSet? beatmapSet = resolved.GetResultSafely()?.BeatmapSet;
|
||||
|
||||
if (beatmapSet == null)
|
||||
return;
|
||||
|
||||
beatmapDownloader.Download(beatmapSet, config.Get<bool>(OsuSetting.PreferNoVideo));
|
||||
}));
|
||||
}
|
||||
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnEntering(e);
|
||||
@@ -527,7 +397,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
client.UserStateChanged -= onUserStateChanged;
|
||||
client.SettingsChanged -= onSettingsChanged;
|
||||
client.LoadRequested -= onLoadRequested;
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
@@ -83,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
initialItem = itemToEdit ?? room.Playlist.LastOrDefault();
|
||||
|
||||
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
|
||||
LeftPadding = new MarginPadding { Top = CORNER_RADIUS_HIDE_OFFSET + Header.HEIGHT };
|
||||
TopPadding = Header.HEIGHT - 10;
|
||||
|
||||
freeModSelect = new FreeModSelectOverlay
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@@ -18,9 +19,12 @@ using osu.Framework.Screens;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online;
|
||||
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;
|
||||
@@ -145,6 +149,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
private FillFlowContainer userStyleSection = null!;
|
||||
private Container<DrawableRoomPlaylistItem> userStyleDisplayContainer = null!;
|
||||
|
||||
private MatchChatDisplay chat = null!;
|
||||
|
||||
private Sample? sampleStart;
|
||||
private IDisposable? userModsSelectOverlayRegistration;
|
||||
|
||||
@@ -221,7 +227,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(content_padding),
|
||||
Padding = new MarginPadding(content_padding) { Top = 10 },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
@@ -273,7 +279,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new OverlinedHeader("Beatmap queue")
|
||||
new SectionHeader(OnlinePlayStrings.MultiplayerBeatmapQueue)
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
@@ -305,7 +311,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
Alpha = 0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OverlinedHeader("Extra mods"),
|
||||
new SectionHeader("Extra mods"),
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
@@ -343,7 +349,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
Alpha = 0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OverlinedHeader("Difficulty"),
|
||||
new SectionHeader(OnlinePlayStrings.Difficulty),
|
||||
userStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
@@ -366,11 +372,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new OverlinedHeader("Chat")
|
||||
new SectionHeader(OnlinePlayStrings.Chat)
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new MatchChatDisplay(room)
|
||||
chat = new MatchChatDisplay(room)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
@@ -431,6 +437,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
client.UserStyleChanged += onUserStyleChanged;
|
||||
client.UserModsChanged += onUserModsChanged;
|
||||
client.LoadRequested += onLoadRequested;
|
||||
client.MatchEvent += onMatchEvent;
|
||||
|
||||
beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true);
|
||||
|
||||
@@ -594,6 +601,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
private void onMatchEvent(MatchServerEvent ev)
|
||||
{
|
||||
switch (ev)
|
||||
{
|
||||
case RollEvent rollEvent:
|
||||
var user = client.Room?.Users.SingleOrDefault(u => u.UserID == rollEvent.UserID)?.User ?? new APIUser { Username = "Unknown user" };
|
||||
string text = $"{user.Username} rolled {"point".ToQuantity(rollEvent.Result)} out of {rollEvent.Max}.";
|
||||
chat.Channel.Value?.AddNewMessages(new InfoMessage(text));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Responds to changes in the local user's beatmap availability to notify the server and prepare the gameplay session.
|
||||
/// </summary>
|
||||
|
||||
@@ -282,7 +282,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
Schedule(() => userModsDisplay.Current.Value = userRuleset == null ? Array.Empty<Mod>() : user.Mods.Select(m => m.ToMod(userRuleset)).ToList());
|
||||
}
|
||||
|
||||
userStateDisplay.UpdateStatus(user.State, user.BeatmapAvailability);
|
||||
userStateDisplay.UpdateStatus(user);
|
||||
|
||||
if (user.BeatmapAvailability.State == DownloadState.LocallyAvailable && user.State != MultiplayerUserState.Spectating)
|
||||
{
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
{
|
||||
public partial class ParticipantsListHeader : OverlinedHeader
|
||||
public partial class ParticipantsListHeader : SectionHeader
|
||||
{
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
if (room == null)
|
||||
return;
|
||||
|
||||
Details.Value = room.Users.Count.ToString();
|
||||
DetailsText.Value = $"{room.Users.Count}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user