1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-23 10:20:07 +08:00

Merge branch 'master' into show-hud-while-editing-skin-layout

This commit is contained in:
Bartłomiej Dach
2025-10-29 14:27:28 +01:00
Unverified
29 changed files with 377 additions and 210 deletions
@@ -29,17 +29,17 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 3, TotalScore = 750 },
], placement_points);
Assert.AreEqual(8, state.Users[1].Points);
Assert.AreEqual(1, state.Users[1].Placement);
Assert.AreEqual(1, state.Users[1].Rounds[1].Placement);
Assert.AreEqual(8, state.Users.GetOrAdd(1).Points);
Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(1, state.Users.GetOrAdd(1).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(6, state.Users[2].Points);
Assert.AreEqual(3, state.Users[2].Placement);
Assert.AreEqual(3, state.Users[2].Rounds[1].Placement);
Assert.AreEqual(6, state.Users.GetOrAdd(2).Points);
Assert.AreEqual(3, state.Users.GetOrAdd(2).Placement);
Assert.AreEqual(3, state.Users.GetOrAdd(2).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(7, state.Users[3].Points);
Assert.AreEqual(2, state.Users[3].Placement);
Assert.AreEqual(2, state.Users[3].Rounds[1].Placement);
Assert.AreEqual(7, state.Users.GetOrAdd(3).Points);
Assert.AreEqual(2, state.Users.GetOrAdd(3).Placement);
Assert.AreEqual(2, state.Users.GetOrAdd(3).Rounds.GetOrAdd(1).Placement);
// 2 -> 1 -> 3
@@ -51,17 +51,17 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 3, TotalScore = 500 },
], placement_points);
Assert.AreEqual(15, state.Users[1].Points);
Assert.AreEqual(1, state.Users[1].Placement);
Assert.AreEqual(2, state.Users[1].Rounds[2].Placement);
Assert.AreEqual(15, state.Users.GetOrAdd(1).Points);
Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(2, state.Users.GetOrAdd(1).Rounds.GetOrAdd(2).Placement);
Assert.AreEqual(14, state.Users[2].Points);
Assert.AreEqual(2, state.Users[2].Placement);
Assert.AreEqual(1, state.Users[2].Rounds[2].Placement);
Assert.AreEqual(14, state.Users.GetOrAdd(2).Points);
Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement);
Assert.AreEqual(1, state.Users.GetOrAdd(2).Rounds.GetOrAdd(2).Placement);
Assert.AreEqual(13, state.Users[3].Points);
Assert.AreEqual(3, state.Users[3].Placement);
Assert.AreEqual(3, state.Users[3].Rounds[2].Placement);
Assert.AreEqual(13, state.Users.GetOrAdd(3).Points);
Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement);
Assert.AreEqual(3, state.Users.GetOrAdd(3).Rounds.GetOrAdd(2).Placement);
}
[Test]
@@ -80,21 +80,21 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 4, TotalScore = 500 },
], placement_points);
Assert.AreEqual(7, state.Users[1].Points);
Assert.AreEqual(1, state.Users[1].Placement);
Assert.AreEqual(2, state.Users[1].Rounds[1].Placement);
Assert.AreEqual(7, state.Users.GetOrAdd(1).Points);
Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(2, state.Users.GetOrAdd(1).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(7, state.Users[2].Points);
Assert.AreEqual(2, state.Users[2].Placement);
Assert.AreEqual(2, state.Users[2].Rounds[1].Placement);
Assert.AreEqual(7, state.Users.GetOrAdd(2).Points);
Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement);
Assert.AreEqual(2, state.Users.GetOrAdd(2).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(5, state.Users[3].Points);
Assert.AreEqual(3, state.Users[3].Placement);
Assert.AreEqual(4, state.Users[3].Rounds[1].Placement);
Assert.AreEqual(5, state.Users.GetOrAdd(3).Points);
Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement);
Assert.AreEqual(4, state.Users.GetOrAdd(3).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(5, state.Users[4].Points);
Assert.AreEqual(4, state.Users[4].Placement);
Assert.AreEqual(4, state.Users[4].Rounds[1].Placement);
Assert.AreEqual(5, state.Users.GetOrAdd(4).Points);
Assert.AreEqual(4, state.Users.GetOrAdd(4).Placement);
Assert.AreEqual(4, state.Users.GetOrAdd(4).Rounds.GetOrAdd(1).Placement);
}
[Test]
@@ -120,8 +120,8 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 2, TotalScore = 1000 },
], placement_points);
Assert.AreEqual(1, state.Users[1].Placement);
Assert.AreEqual(2, state.Users[2].Placement);
Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement);
}
[Test]
@@ -142,12 +142,12 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 5, TotalScore = 1000 },
], placement_points);
Assert.AreEqual(1, state.Users[1].Placement);
Assert.AreEqual(2, state.Users[2].Placement);
Assert.AreEqual(3, state.Users[3].Placement);
Assert.AreEqual(4, state.Users[4].Placement);
Assert.AreEqual(5, state.Users[5].Placement);
Assert.AreEqual(6, state.Users[6].Placement);
Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement);
Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement);
Assert.AreEqual(4, state.Users.GetOrAdd(4).Placement);
Assert.AreEqual(5, state.Users.GetOrAdd(5).Placement);
Assert.AreEqual(6, state.Users.GetOrAdd(6).Placement);
}
}
}
@@ -35,7 +35,9 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
protected override Drawable CreateDefaultImplementation() => new ArgonKeyCounterDisplay();
protected override Drawable CreateArgonImplementation() => new ArgonKeyCounterDisplay();
protected override Drawable CreateDefaultImplementation() => new DefaultKeyCounterDisplay();
protected override Drawable CreateLegacyImplementation() => new LegacyKeyCounterDisplay();
}
@@ -1,11 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
@@ -62,5 +64,41 @@ namespace osu.Game.Tests.Visual.Matchmaking
panel.AllowSelection = value;
});
}
[Test]
public void TestFailedBeatmapLookup()
{
AddStep("setup request handle", () =>
{
var api = (DummyAPIAccess)API;
var handler = api.HandleRequest;
api.HandleRequest = req =>
{
switch (req)
{
case GetBeatmapRequest:
case GetBeatmapsRequest:
req.TriggerFailure(new InvalidOperationException());
return false;
default:
return handler?.Invoke(req) ?? false;
}
};
});
AddStep("add panel", () =>
{
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new BeatmapSelectPanel(new MultiplayerPlaylistItem())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
});
}
}
}
@@ -124,11 +124,11 @@ namespace osu.Game.Tests.Visual.Matchmaking
foreach (var user in MultiplayerClient.ServerRoom!.Users.OrderBy(_ => RNG.Next()))
{
state.Users[user.UserID].Placement = i++;
state.Users[user.UserID].Points = (8 - i) * 7;
state.Users[user.UserID].Rounds[1].Placement = 1;
state.Users[user.UserID].Rounds[1].TotalScore = 1;
state.Users[user.UserID].Rounds[1].Statistics[HitResult.LargeBonus] = 1;
state.Users.GetOrAdd(user.UserID).Placement = i++;
state.Users.GetOrAdd(user.UserID).Points = (8 - i) * 7;
state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).Placement = 1;
state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).TotalScore = 1;
state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).Statistics[HitResult.LargeBonus] = 1;
}
});
}
@@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
MatchmakingRoomState state = new MatchmakingRoomState();
for (int i = 0; i < room.Users.Count; i++)
state.Users[room.Users[i].UserID].Placement = placements[i];
state.Users.GetOrAdd(room.Users[i].UserID).Placement = placements[i];
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});
@@ -36,28 +36,28 @@ namespace osu.Game.Tests.Visual.Matchmaking
int localUserId = API.LocalUser.Value.OnlineID;
// Overall state.
state.Users[localUserId].Placement = 1;
state.Users[localUserId].Points = 8;
state.Users.GetOrAdd(localUserId).Placement = 1;
state.Users.GetOrAdd(localUserId).Points = 8;
for (int round = 1; round <= state.CurrentRound; round++)
state.Users[localUserId].Rounds[round].Placement = round;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(round).Placement = round;
// Highest score.
state.Users[localUserId].Rounds[1].TotalScore = 1000;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(1).TotalScore = 1000;
// Highest accuracy.
state.Users[localUserId].Rounds[2].Accuracy = 0.9995;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(2).Accuracy = 0.9995;
// Highest combo.
state.Users[localUserId].Rounds[3].MaxCombo = 100;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(3).MaxCombo = 100;
// Most bonus score.
state.Users[localUserId].Rounds[4].Statistics[HitResult.LargeBonus] = 50;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 50;
// Smallest score difference.
state.Users[localUserId].Rounds[5].TotalScore = 1000;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(5).TotalScore = 1000;
// Largest score difference.
state.Users[localUserId].Rounds[6].TotalScore = 1000;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(6).TotalScore = 1000;
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});
@@ -103,36 +103,51 @@ namespace osu.Game.Tests.Visual.Matchmaking
int localUserId = API.LocalUser.Value.OnlineID;
// Overall state.
state.Users[localUserId].Placement = 1;
state.Users[localUserId].Points = 8;
state.Users[invalid_user_id].Placement = 2;
state.Users[invalid_user_id].Points = 7;
state.Users.GetOrAdd(localUserId).Placement = 1;
state.Users.GetOrAdd(localUserId).Points = 8;
state.Users.GetOrAdd(invalid_user_id).Placement = 2;
state.Users.GetOrAdd(invalid_user_id).Points = 7;
for (int round = 1; round <= state.CurrentRound; round++)
state.Users[localUserId].Rounds[round].Placement = round;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(round).Placement = round;
// Highest score.
state.Users[localUserId].Rounds[1].TotalScore = 1000;
state.Users[invalid_user_id].Rounds[1].TotalScore = 990;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(1).TotalScore = 1000;
state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(1).TotalScore = 990;
// Highest accuracy.
state.Users[localUserId].Rounds[2].Accuracy = 0.9995;
state.Users[invalid_user_id].Rounds[2].Accuracy = 0.5;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(2).Accuracy = 0.9995;
state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(2).Accuracy = 0.5;
// Highest combo.
state.Users[localUserId].Rounds[3].MaxCombo = 100;
state.Users[invalid_user_id].Rounds[3].MaxCombo = 10;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(3).MaxCombo = 100;
state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(3).MaxCombo = 10;
// Most bonus score.
state.Users[localUserId].Rounds[4].Statistics[HitResult.LargeBonus] = 50;
state.Users[invalid_user_id].Rounds[4].Statistics[HitResult.LargeBonus] = 25;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 50;
state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 25;
// Smallest score difference.
state.Users[localUserId].Rounds[5].TotalScore = 1000;
state.Users[invalid_user_id].Rounds[5].TotalScore = 999;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(5).TotalScore = 1000;
state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(5).TotalScore = 999;
// Largest score difference.
state.Users[localUserId].Rounds[6].TotalScore = 1000;
state.Users[invalid_user_id].Rounds[6].TotalScore = 0;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(6).TotalScore = 1000;
state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(6).TotalScore = 0;
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});
}
[Test]
public void TestNoUsers()
{
AddStep("show results with no users", () =>
{
var state = new MatchmakingRoomState
{
CurrentRound = 6,
Stage = MatchmakingStage.Ended
};
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});
@@ -2,9 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
@@ -13,25 +16,35 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneDrawableDate : OsuTestScene
{
public TestSceneDrawableDate()
[SetUpSteps]
public void SetUpSteps()
{
Child = new FillFlowContainer
AddStep("Create 7 dates", () =>
{
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Children = new Drawable[]
Child = new FillFlowContainer
{
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(60))),
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(55))),
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(50))),
new PokeyDrawableDate(DateTimeOffset.Now),
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(60))),
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(65))),
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(70))),
}
};
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Children = new Drawable[]
{
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(60))),
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(55))),
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(50))),
new PokeyDrawableDate(DateTimeOffset.Now),
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(60))),
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(65))),
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(70))),
}
};
});
}
[Test]
public void TestSecondsUpdate()
{
AddUntilStep("4th date says \"2 seconds ago\"", () => this.ChildrenOfType<DrawableDate>().ElementAt(3).Current.Value == "2 seconds ago");
}
private partial class PokeyDrawableDate : CompositeDrawable
+14 -6
View File
@@ -5,6 +5,7 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Utils;
@@ -80,7 +81,7 @@ namespace osu.Game.Graphics
public DateTimeOffset TooltipContent => Date;
private class HumanisedDate : IEquatable<HumanisedDate>, ILocalisableStringData
private class HumanisedDate : ILocalisableStringData
{
public readonly DateTimeOffset Date;
@@ -89,11 +90,18 @@ namespace osu.Game.Graphics
Date = date;
}
public bool Equals(HumanisedDate? other)
=> other?.Date != null && Date.Equals(other.Date);
public bool Equals(ILocalisableStringData? other)
=> other is HumanisedDate otherDate && Equals(otherDate);
/// <remarks>
/// Humanizer formats the <see cref="Date"/> relative to the local computer time.
/// Therefore, replacing a <see cref="HumanisedDate"/> instance with another instance of the class with the same <see cref="Date"/>
/// should have the effect of replacing and re-formatting the text.
/// Including <see cref="Date"/> in equality members would stop this from happening, as <see cref="SpriteText.Text"/>
/// has equality-based early guards to prevent redundant text replaces.
/// Thus, instances of these class just compare <see langword="false"/> to any <see cref="ILocalisableStringData"/> to ensure re-formatting happens correctly.
/// There are "technically" more "correct" ways to do this (like also including the current time into equality checks),
/// but they are simultaneously functionally equivalent to this and overly convoluted.
/// This is a private hack-job of a wrapper around humanizer anyway.
/// </remarks>
public bool Equals(ILocalisableStringData? other) => false;
public string GetLocalised(LocalisationParameters parameters) => HumanizerUtils.Humanize(Date);
@@ -89,13 +89,13 @@ namespace osu.Game.Online.Metadata
userStatus.BindValueChanged(status =>
{
if (localUser.Value is not GuestUser)
UpdateStatus(status.NewValue);
UpdateStatus(status.NewValue).FireAndForget();
}, true);
userActivity.BindValueChanged(activity =>
{
if (localUser.Value is not GuestUser)
UpdateActivity(activity.NewValue);
UpdateActivity(activity.NewValue).FireAndForget();
}, true);
}
@@ -121,8 +121,8 @@ namespace osu.Game.Online.Metadata
if (localUser.Value is not GuestUser)
{
UpdateActivity(userActivity.Value);
UpdateStatus(userStatus.Value);
UpdateActivity(userActivity.Value).FireAndForget();
UpdateStatus(userStatus.Value).FireAndForget();
}
if (lastQueueId.Value >= 0)
@@ -81,10 +81,10 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
foreach (var score in scoreGroup)
{
MatchmakingUser mmUser = Users[score.UserID];
MatchmakingUser mmUser = Users.GetOrAdd(score.UserID);
mmUser.Points += placementPoints[placement - 1];
MatchmakingRound mmRound = mmUser.Rounds[CurrentRound];
MatchmakingRound mmRound = mmUser.Rounds.GetOrAdd(CurrentRound);
mmRound.Placement = placement;
mmRound.TotalScore = score.TotalScore;
mmRound.Accuracy = score.Accuracy;
@@ -21,27 +21,24 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
[Key(0)]
public IDictionary<int, MatchmakingRound> RoundsDictionary { get; set; } = new Dictionary<int, MatchmakingRound>();
/// <summary>
/// Creates or retrieves the score for the given round.
/// </summary>
/// <param name="round">The round.</param>
public MatchmakingRound this[int round]
{
get
{
if (RoundsDictionary.TryGetValue(round, out MatchmakingRound? score))
return score;
return RoundsDictionary[round] = new MatchmakingRound { Round = round };
}
}
/// <summary>
/// The total number of rounds.
/// </summary>
[IgnoreMember]
public int Count => RoundsDictionary.Count;
/// <summary>
/// Retrieves or adds a <see cref="MatchmakingRound"/> entry to this list.
/// </summary>
/// <param name="round">The round.</param>
public MatchmakingRound GetOrAdd(int round)
{
if (RoundsDictionary.TryGetValue(round, out MatchmakingRound? score))
return score;
return RoundsDictionary[round] = new MatchmakingRound { Round = round };
}
public IEnumerator<MatchmakingRound> GetEnumerator() => RoundsDictionary.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
@@ -23,7 +23,7 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
/// The aggregate room placement (1-based).
/// </summary>
[Key(1)]
public int Placement { get; set; }
public int? Placement { get; set; }
/// <summary>
/// The aggregate points.
@@ -21,27 +21,24 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
[Key(0)]
public IDictionary<int, MatchmakingUser> UserDictionary { get; set; } = new Dictionary<int, MatchmakingUser>();
/// <summary>
/// Creates or retrieves the user for the given id.
/// </summary>
/// <param name="userId">The user id.</param>
public MatchmakingUser this[int userId]
{
get
{
if (UserDictionary.TryGetValue(userId, out MatchmakingUser? user))
return user;
return UserDictionary[userId] = new MatchmakingUser { UserId = userId };
}
}
/// <summary>
/// The total number of users.
/// </summary>
[IgnoreMember]
public int Count => UserDictionary.Count;
/// <summary>
/// Retrieves or adds a <see cref="MatchmakingUser"/> entry to this list.
/// </summary>
/// <param name="userId">The user ID.</param>
public MatchmakingUser GetOrAdd(int userId)
{
if (UserDictionary.TryGetValue(userId, out MatchmakingUser? user))
return user;
return UserDictionary[userId] = new MatchmakingUser { UserId = userId };
}
public IEnumerator<MatchmakingUser> GetEnumerator() => UserDictionary.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
@@ -201,7 +201,7 @@ namespace osu.Game.Online.Multiplayer
if (!connected.NewValue)
{
if (Room != null)
LeaveRoom();
LeaveRoom().FireAndForget();
MatchmakingQueueLeft?.Invoke();
}
@@ -560,7 +560,7 @@ namespace osu.Game.Online.Multiplayer
return;
if (user.Equals(LocalUser))
LeaveRoom();
LeaveRoom().FireAndForget();
handleUserLeft(user, UserKicked);
});
+5 -4
View File
@@ -14,6 +14,7 @@ using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
@@ -203,7 +204,7 @@ namespace osu.Game.Online.Spectator
Task IStatefulUserHubClient.DisconnectRequested()
{
Schedule(() => DisconnectInternal());
Schedule(() => DisconnectInternal().FireAndForget());
return Task.CompletedTask;
}
@@ -290,7 +291,7 @@ namespace osu.Game.Online.Spectator
else
currentState.State = SpectatedUserState.Quit;
EndPlayingInternal(currentState);
EndPlayingInternal(currentState).FireAndForget();
});
}
@@ -304,7 +305,7 @@ namespace osu.Game.Online.Spectator
return;
}
WatchUserInternal(userId);
WatchUserInternal(userId).FireAndForget();
}
public void StopWatchingUser(int userId)
@@ -321,7 +322,7 @@ namespace osu.Game.Online.Spectator
watchedUsersRefCounts.Remove(userId);
watchedUserStates.Remove(userId);
StopWatchingUserInternal(userId);
StopWatchingUserInternal(userId).FireAndForget();
});
}
+5 -2
View File
@@ -162,16 +162,17 @@ namespace osu.Game.Overlays
private int runningDepth;
private readonly Scheduler postScheduler = new Scheduler();
private readonly Scheduler criticalPostScheduler = new Scheduler();
public override bool IsPresent =>
// Delegate presence as we need to consider the toast tray in addition to the main overlay.
State.Value == Visibility.Visible || mainContent.IsPresent || toastTray.IsPresent || postScheduler.HasPendingTasks;
State.Value == Visibility.Visible || mainContent.IsPresent || toastTray.IsPresent || postScheduler.HasPendingTasks || criticalPostScheduler.HasPendingTasks;
private bool processingPosts = true;
private double? lastSamplePlayback;
public void Post(Notification notification) => postScheduler.Add(() =>
public void Post(Notification notification) => (notification.IsCritical ? criticalPostScheduler : postScheduler).Add(() =>
{
++runningDepth;
@@ -220,6 +221,8 @@ namespace osu.Game.Overlays
{
base.Update();
criticalPostScheduler.Update();
if (processingPosts)
postScheduler.Update();
}
@@ -91,7 +91,12 @@ namespace osu.Game.Overlays
public void FlushAllToasts()
{
foreach (var notification in toastFlow.ToArray())
{
if (notification.IsCritical)
continue;
forwardNotification(notification);
}
}
public void Post(Notification notification)
@@ -39,6 +39,11 @@ namespace osu.Game.Overlays.Notifications
/// </summary>
public bool IsImportant { get; init; } = true;
/// <summary>
/// Critical notifications show even during gameplay or other scenarios where notifications would usually be suppressed.
/// </summary>
public bool IsCritical { get; init; }
/// <summary>
/// Transient notifications only show as a toast, and do not linger in notification history.
/// </summary>
+25 -13
View File
@@ -50,7 +50,7 @@ namespace osu.Game.Screens.Footer
private Box background = null!;
private FillFlowContainer<ScreenFooterButton> buttonsFlow = null!;
private Container footerContentContainer = null!;
private Container overlayContentContainer = null!;
private Container<ScreenFooterButton> hiddenButtonsContainer = null!;
private LogoTrackingContainer logoTrackingContainer = null!;
@@ -102,6 +102,7 @@ namespace osu.Game.Screens.Footer
{
buttonsFlow = new FillFlowContainer<ScreenFooterButton>
{
Name = "Visible footer buttons",
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Y = ScreenFooterButton.CORNER_RADIUS,
@@ -109,8 +110,9 @@ namespace osu.Game.Screens.Footer
Spacing = new Vector2(7, 0),
AutoSizeAxes = Axes.Both,
},
footerContentContainer = new Container
overlayContentContainer = new Container
{
Name = "Overlay-provided extra content",
RelativeSizeAxes = Axes.Both,
Y = -OsuGame.SCREEN_EDGE_MARGIN,
},
@@ -126,6 +128,7 @@ namespace osu.Game.Screens.Footer
},
hiddenButtonsContainer = new Container<ScreenFooterButton>
{
Name = "Hidden footer buttons",
Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding },
Y = ScreenFooterButton.CORNER_RADIUS,
Anchor = Anchor.BottomLeft,
@@ -234,11 +237,11 @@ namespace osu.Game.Screens.Footer
public ShearedOverlayContainer? ActiveOverlay { get; private set; }
private VisibilityContainer? activeFooterContent;
private VisibilityContainer? activeOverlayContent;
private readonly List<ScreenFooterButton> temporarilyHiddenButtons = new List<ScreenFooterButton>();
public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? footerContent)
public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? overlayContent)
{
if (ActiveOverlay != null)
{
@@ -267,12 +270,12 @@ namespace osu.Game.Screens.Footer
updateColourScheme(overlay.ColourProvider.Hue);
footerContent = overlay.CreateFooterContent();
activeFooterContent = footerContent;
var content = footerContent;
overlayContent = overlay.CreateFooterContent();
activeOverlayContent = overlayContent;
var content = overlayContent;
if (content != null)
footerContentContainer.Child = content;
overlayContentContainer.Child = content;
if (temporarilyHiddenButtons.Count > 0)
this.Delay(60).Schedule(() => content?.Show());
@@ -287,15 +290,19 @@ namespace osu.Game.Screens.Footer
if (ActiveOverlay == null)
return;
Debug.Assert(activeFooterContent != null);
activeFooterContent.Hide();
Debug.Assert(activeOverlayContent != null);
activeOverlayContent.Hide();
double timeUntilRun = activeFooterContent.LatestTransformEndTime - Time.Current;
double timeUntilRun = activeOverlayContent.LatestTransformEndTime - Time.Current;
for (int i = 0; i < temporarilyHiddenButtons.Count; i++)
{
var button = temporarilyHiddenButtons[i];
hiddenButtonsContainer.Remove(button, false);
// temporarily bypass autosize on the X axis to prevent the buttons taking space
// immediately upon being moved back to the flow.
// this prevents the overlay content jumping to the right during its fade-out.
button.BypassAutoSizeAxes = Axes.X;
buttonsFlow.Add(button);
makeButtonAppearFromBottom(button, 0);
@@ -305,8 +312,13 @@ namespace osu.Game.Screens.Footer
updateColourScheme(OverlayColourScheme.Aquamarine.GetHue());
activeFooterContent.Delay(timeUntilRun).Expire();
activeFooterContent = null;
activeOverlayContent.Delay(timeUntilRun).Schedule(() =>
{
// overlay content is done displaying, re-enable autosize on all active buttons
foreach (var button in buttonsFlow)
button.BypassAutoSizeAxes = Axes.None;
}).Expire();
activeOverlayContent = null;
ActiveOverlay = null;
}
@@ -111,7 +111,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
Debug.Assert(card == null);
var beatmap = b.GetResultSafely()!;
APIBeatmap beatmap = b.GetResultSafely() ?? new APIBeatmap
{
BeatmapSet = new APIBeatmapSet
{
Title = "unknown beatmap",
TitleUnicode = "unknown beatmap",
Artist = "unknown artist",
ArtistUnicode = "unknown artist",
}
};
beatmap.StarRating = Item.StarRating;
mainContent.Add(card = new BeatmapCardMatchmaking(beatmap)
@@ -414,8 +414,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
if (!matchmakingState.Users.UserDictionary.TryGetValue(User.Id, out MatchmakingUser? userScore))
return;
rankText.Text = userScore.Placement.Ordinalize(CultureInfo.CurrentCulture);
rankText.FadeColour(SubScreenResults.ColourForPlacement(userScore.Placement));
if (userScore.Placement == null)
return;
rankText.Text = userScore.Placement.Value.Ordinalize(CultureInfo.CurrentCulture);
rankText.FadeColour(SubScreenResults.ColourForPlacement(userScore.Placement.Value));
scoreText.Text = $"{userScore.Points} pts";
});
@@ -239,8 +239,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
if (client.Room?.MatchState is not MatchmakingRoomState matchmakingState)
continue;
if (matchmakingState.Users.UserDictionary.TryGetValue(panels[i].User.Id, out MatchmakingUser? user))
SetLayoutPosition(Children[i], user.Placement);
if (matchmakingState.Users.UserDictionary.TryGetValue(panels[i].User.Id, out MatchmakingUser? user) && user.Placement != null)
SetLayoutPosition(Children[i], user.Placement.Value);
else
SetLayoutPosition(Children[i], float.MaxValue);
}
@@ -194,20 +194,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
{
userStatistics.Clear();
if (state.Users[client.LocalUser!.UserID].Rounds.Count == 0)
var localUserState = state.Users.GetOrAdd(client.LocalUser!.UserID);
if (localUserState.Rounds.Count == 0)
{
placementText.Text = "-";
placementText.Colour = OsuColour.Gray(1f);
return;
}
int overallPlacement = state.Users[client.LocalUser!.UserID].Placement;
int? overallPlacement = localUserState.Placement;
placementText.Text = overallPlacement.Ordinalize(CultureInfo.CurrentCulture);
placementText.Colour = ColourForPlacement(overallPlacement);
if (overallPlacement != null)
{
placementText.Text = overallPlacement.Value.Ordinalize(CultureInfo.CurrentCulture);
placementText.Colour = ColourForPlacement(overallPlacement.Value);
int overallPoints = state.Users[client.LocalUser!.UserID].Points;
addStatistic(overallPlacement, $"Overall position ({overallPoints} points)");
int overallPoints = localUserState.Points;
addStatistic(overallPlacement.Value, $"Overall position ({overallPoints} points)");
}
var accuracyOrderedUsers = state.Users.Select(u => (user: u, avgAcc: u.Rounds.Select(r => r.Accuracy).DefaultIfEmpty(0).Average()))
.OrderByDescending(t => t.avgAcc)
@@ -223,7 +228,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
int maxComboPlacement = maxComboOrderedUsers.index + 1;
addStatistic(maxComboPlacement, $"Best max combo ({maxComboOrderedUsers.info.maxCombo}x)");
var bestPlacement = state.Users[client.LocalUser!.UserID].Rounds.MinBy(r => r.Placement);
var bestPlacement = localUserState.Rounds.MinBy(r => r.Placement);
addStatistic(bestPlacement!.Placement, $"Best round placement (round {bestPlacement.Round})");
void addStatistic(int position, string text) => userStatistics.Add(new PanelUserStatistic(position, text));
@@ -255,27 +260,27 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
roomAwards.Clear();
long maxScore = long.MinValue;
int maxScoreUserId = 0;
int maxScoreUserId = -1;
double maxAccuracy = double.MinValue;
int maxAccuracyUserId = 0;
int maxAccuracyUserId = -1;
int maxCombo = int.MinValue;
int maxComboUserId = 0;
int maxComboUserId = -1;
long maxBonusScore = 0;
int maxBonusScoreUserId = 0;
int maxBonusScoreUserId = -1;
long largestScoreDifference = long.MinValue;
int largestScoreDifferenceUserId = 0;
int largestScoreDifferenceUserId = -1;
long smallestScoreDifference = long.MaxValue;
int smallestScoreDifferenceUserId = 0;
int smallestScoreDifferenceUserId = -1;
for (int round = 1; round <= state.CurrentRound; round++)
{
long roundHighestScore = long.MinValue;
int roundHighestScoreUserId = 0;
int roundHighestScoreUserId = -1;
long roundLowestScore = long.MaxValue;
@@ -344,11 +349,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
}
}
addAward(maxScoreUserId, "Score champ", "Highest score in a single round");
if (maxScoreUserId > 0)
addAward(maxScoreUserId, "Score champ", "Highest score in a single round");
addAward(maxAccuracyUserId, "Most accurate", "Highest accuracy in a single round");
if (maxAccuracyUserId > 0)
addAward(maxAccuracyUserId, "Most accurate", "Highest accuracy in a single round");
addAward(maxComboUserId, "Top combo", "Highest combo in a single round");
if (maxComboUserId > 0)
addAward(maxComboUserId, "Top combo", "Highest combo in a single round");
if (maxBonusScoreUserId > 0)
addAward(maxBonusScoreUserId, "Biggest bonus", "Biggest bonus score across all rounds");
@@ -392,7 +392,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
return;
}
client.ChangeState(MultiplayerUserState.Idle);
client.ChangeState(MultiplayerUserState.Idle).FireAndForget();
}
/// <summary>
@@ -2,10 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
@@ -32,11 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
[Resolved]
private INotificationOverlay? notifications { get; set; }
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
private ProgressNotification? backgroundNotification;
private Notification? readyNotification;
private BackgroundQueueNotification? backgroundNotification;
private bool isBackgrounded;
protected override void LoadComplete()
@@ -118,27 +119,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
if (backgroundNotification != null)
return;
notifications?.Post(backgroundNotification = new ProgressNotification
{
Text = "Searching for opponents...",
CompletionTarget = n => notifications.Post(readyNotification = n),
CompletionText = "Your match is ready! Click to join.",
CompletionClickAction = () =>
{
client.MatchmakingAcceptInvitation().FireAndForget();
performer?.PerformFromScreen(s => s.Push(new IntroScreen()));
closeNotifications();
return true;
},
CancelRequested = () =>
{
client.MatchmakingLeaveQueue().FireAndForget();
closeNotifications();
return true;
}
});
notifications?.Post(backgroundNotification = new BackgroundQueueNotification());
}
private void closeNotifications()
@@ -146,13 +127,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
if (backgroundNotification != null)
{
backgroundNotification.State = ProgressNotificationState.Cancelled;
backgroundNotification.Close(false);
backgroundNotification.CloseAll();
backgroundNotification = null;
}
readyNotification?.Close(false);
backgroundNotification = null;
readyNotification = null;
}
protected override void Dispose(bool isDisposing)
@@ -168,5 +145,78 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
client.MatchmakingRoomReady -= onMatchmakingRoomReady;
}
}
private partial class BackgroundQueueNotification : ProgressNotification
{
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
[Resolved]
private MultiplayerClient client { get; set; } = null!;
private Notification? foundNotification;
private Sample? matchFoundSample;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
Text = "Searching for opponents...";
CompletionClickAction = () =>
{
client.MatchmakingAcceptInvitation().FireAndForget();
performer?.PerformFromScreen(s => s.Push(new IntroScreen()));
Close(false);
return true;
};
CancelRequested = () =>
{
client.MatchmakingLeaveQueue().FireAndForget();
return true;
};
matchFoundSample = audio.Samples.Get(@"Multiplayer/Matchmaking/match-found");
}
protected override Notification CreateCompletionNotification()
{
// Playing here means it will play even if notification overlay is hidden.
//
// If we add support for the completion notification to be processed during gameplay,
// this can be moved inside the `MatchFoundNotification` implementation.
matchFoundSample?.Play();
return foundNotification = new MatchFoundNotification
{
Activated = CompletionClickAction,
Text = "Your match is ready! Click to join.",
};
}
public void CloseAll()
{
foundNotification?.Close(false);
Close(false);
}
public partial class MatchFoundNotification : ProgressCompletionNotification
{
protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times;
public MatchFoundNotification()
{
IsCritical = true;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Icon = FontAwesome.Solid.Bolt;
IconContent.Colour = ColourInfo.GradientVertical(colours.YellowDark, colours.YellowLight);
}
}
}
}
}
@@ -87,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Debug.Assert(client.LocalUser != null);
if (client.LocalUser.State == MultiplayerUserState.Results)
client.ChangeState(MultiplayerUserState.Idle);
client.ChangeState(MultiplayerUserState.Idle).FireAndForget();
}
protected override string ScreenTitle => "Multiplayer";
@@ -618,7 +618,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
updateGameplayState();
if (client.LocalUser.State == MultiplayerUserState.Ready)
client.ChangeState(MultiplayerUserState.Idle);
client.ChangeState(MultiplayerUserState.Idle).FireAndForget();
break;
}
}
@@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.LocalUser?.State == MultiplayerUserState.Loaded)
{
loadingDisplay.Show();
client.ChangeState(MultiplayerUserState.ReadyForGameplay);
client.ChangeState(MultiplayerUserState.ReadyForGameplay).FireAndForget();
}
// This will pause the clock, pending the gameplay started callback from the server.
@@ -296,7 +296,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
// On a manual exit, set the player back to idle unless gameplay has finished.
// Of note, this doesn't cover exiting using alt-f4 or menu home option.
if (multiplayerClient.Room.State != MultiplayerRoomState.Open)
multiplayerClient.ChangeState(MultiplayerUserState.Idle);
multiplayerClient.ChangeState(MultiplayerUserState.Idle).FireAndForget();
return base.OnBackButton();
}