diff --git a/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs b/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs index 5f82d22ae8..25766d4645 100644 --- a/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs +++ b/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using NUnit.Framework; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; @@ -149,5 +150,33 @@ namespace osu.Game.Tests.Online.Matchmaking Assert.AreEqual(5, state.Users.GetOrAdd(5).Placement); Assert.AreEqual(6, state.Users.GetOrAdd(6).Placement); } + + [Test] + public void AbandonOrder() + { + var state = new MatchmakingRoomState(); + + state.AdvanceRound(); + state.RecordScores( + [ + new SoloScoreInfo { UserID = 1, TotalScore = 1000 }, + new SoloScoreInfo { UserID = 2, TotalScore = 500 }, + ], placement_points); + + Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement); + + state.Users.GetOrAdd(1).AbandonedAt = DateTimeOffset.Now; + state.RecordScores([], placement_points); + + Assert.AreEqual(2, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(1, state.Users.GetOrAdd(2).Placement); + + state.Users.GetOrAdd(2).AbandonedAt = DateTimeOffset.Now - TimeSpan.FromMinutes(1); + state.RecordScores([], placement_points); + + Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement); + } } } diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs index ac97b114d8..94062d6024 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs @@ -36,5 +36,11 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking /// [Key(3)] public MatchmakingRoundList Rounds { get; set; } = new MatchmakingRoundList(); + + /// + /// The time at which this user abandoned the match. + /// + [Key(4)] + public DateTimeOffset? AbandonedAt { get; set; } } } diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserComparer.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserComparer.cs index 74da6a9b2a..a81c49fe97 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserComparer.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserComparer.cs @@ -23,42 +23,53 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking ArgumentNullException.ThrowIfNull(x); ArgumentNullException.ThrowIfNull(y); - // X appears earlier in the list if it has more points. - if (x.Points > y.Points) - return -1; + int compare = compareAbandonedAt(x, y); + if (compare != 0) + return compare; - // Y appears earlier in the list if it has more points. - if (y.Points > x.Points) - return 1; + compare = comparePoints(x, y); + if (compare != 0) + return compare; - // Tiebreaker 1 (likely): From each user's point-of-view, their earliest and best placement. + compare = compareRoundPlacements(x, y); + if (compare != 0) + return compare; + + return compareUserIds(x, y); + } + + private int compareAbandonedAt(MatchmakingUser x, MatchmakingUser y) + { + DateTimeOffset xAbandonedAt = x.AbandonedAt ?? DateTimeOffset.MaxValue; + DateTimeOffset yAbandonedAt = y.AbandonedAt ?? DateTimeOffset.MaxValue; + return -xAbandonedAt.CompareTo(yAbandonedAt); + } + + private int comparePoints(MatchmakingUser x, MatchmakingUser y) + { + return -x.Points.CompareTo(y.Points); + } + + private int compareRoundPlacements(MatchmakingUser x, MatchmakingUser y) + { for (int r = 1; r <= rounds; r++) { - MatchmakingRound? xRound; - x.Rounds.RoundsDictionary.TryGetValue(r, out xRound); + x.Rounds.RoundsDictionary.TryGetValue(r, out var xRound); + y.Rounds.RoundsDictionary.TryGetValue(r, out var yRound); - MatchmakingRound? yRound; - y.Rounds.RoundsDictionary.TryGetValue(r, out yRound); + int xPlacement = xRound?.Placement ?? int.MaxValue; + int yPlacement = yRound?.Placement ?? int.MaxValue; - // Nothing to do if both players haven't played this round. - if (xRound == null && yRound == null) - continue; - - // X appears later in the list if it hasn't played this round. - if (xRound == null) - return 1; - - // Y appears later in the list if it hasn't played this round. - if (yRound == null) - return -1; - - // X appears earlier in the list if it has a better placement in the round. - int compare = xRound.Placement.CompareTo(yRound.Placement); + int compare = xPlacement.CompareTo(yPlacement); if (compare != 0) return compare; } - // Tiebreaker 2 (unlikely): User ID. + return 0; + } + + private int compareUserIds(MatchmakingUser x, MatchmakingUser y) + { return x.UserId.CompareTo(y.UserId); } }