10 Commits

Author SHA1 Message Date
d76d4d9a35 okay, i messed things up 2025-12-09 20:20:07 +03:00
Bartłomiej Dach
bb7417c099 Filter out more exceptions from being sent to sentry
More or less covers the first page of client sentry issues sorted by
volume, all of which is pretty much useless for anything because it's
client-specific-failure noise.
2025-12-09 20:19:16 +03:00
Dan Balasescu
066e093987 Adjust vote-to-skip to be explicit about states 2025-12-09 20:19:16 +03:00
Dan Balasescu
56e0c3e65d Fix potentially unsafe quick play event handling 2025-12-09 20:19:16 +03:00
Natelytle
89d7726903 Rank swap mod 2025-12-09 20:19:16 +03:00
Dan Balasescu
36f1bfef07 Fix incorrect quick play download progress 2025-12-09 20:19:15 +03:00
Dan Balasescu
118f07878a Allow score panel to animate 2025-12-09 20:19:15 +03:00
Dan Balasescu
e144968893 Remove quick play round results scroll animation 2025-12-09 20:19:15 +03:00
Dan Balasescu
d2ffea41c6 Consider abandon time for user placements 2025-12-09 20:19:15 +03:00
Dan Balasescu
a8be9b1381 Make quick play chat retain focus after posting 2025-12-09 20:19:15 +03:00
19 changed files with 171 additions and 110 deletions

View File

@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1205.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1209.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModRandom)).ToArray();
public override bool Ranked => true;
public void ApplyToBeatmap(IBeatmap beatmap)
{

View File

@@ -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 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);
}
}
}

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Utils;
@@ -26,8 +27,15 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
WaitForJoined();
}
setupRequestHandler();
[TestCase(2)]
[TestCase(4)]
[TestCase(8)]
[TestCase(16)]
public void TestDisplayScores(int scoreCount)
{
setupRequestHandler(scoreCount);
AddStep("load screen", () =>
{
@@ -40,7 +48,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
});
}
private void setupRequestHandler()
private void setupRequestHandler(int scoreCount)
{
AddStep("setup request handler", () =>
{
@@ -71,7 +79,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
case IndexPlaylistScoresRequest index:
var result = new IndexedMultiplayerScores();
for (int i = 0; i < 8; ++i)
for (int i = 0; i < scoreCount; ++i)
{
result.Scores.Add(new MultiplayerScore
{

View File

@@ -603,7 +603,7 @@ namespace osu.Game.Online.API
cancellationToken.Cancel();
}
private class WebRequestFlushedException : Exception
internal class WebRequestFlushedException : Exception
{
public WebRequestFlushedException(APIState state)
: base($@"Request failed from flush operation (state {state})")

View File

@@ -153,7 +153,7 @@ namespace osu.Game.Online.Multiplayer
/// <summary>
/// Signals that a user has requested to skip the beatmap intro.
/// </summary>
Task UserVotedToSkipIntro(int userId);
Task UserVotedToSkipIntro(int userId, bool voted);
/// <summary>
/// Signals that the vote to skip the beatmap intro has passed.

View File

@@ -36,5 +36,11 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
/// </summary>
[Key(3)]
public MatchmakingRoundList Rounds { get; set; } = new MatchmakingRoundList();
/// <summary>
/// The time at which this user abandoned the match.
/// </summary>
[Key(4)]
public DateTimeOffset? AbandonedAt { get; set; }
}
}

View File

@@ -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);
}
}

View File

@@ -131,7 +131,7 @@ namespace osu.Game.Online.Multiplayer
public event Action<int, long>? MatchmakingItemDeselected;
public event Action<MatchRoomState>? MatchRoomStateChanged;
public event Action<int>? UserVotedToSkipIntro;
public event Action<int, bool>? UserVotedToSkipIntro;
public event Action? VoteToSkipIntroPassed;
public event Action<MultiplayerRoomUser, BeatmapAvailability>? BeatmapAvailabilityChanged;
@@ -854,10 +854,6 @@ namespace osu.Game.Online.Multiplayer
handleRoomRequest(() =>
{
Debug.Assert(Room != null);
foreach (var user in Room.Users)
user.VotedToSkipIntro = false;
GameplayStarted?.Invoke();
});
@@ -928,7 +924,7 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask;
}
Task IMultiplayerClient.UserVotedToSkipIntro(int userId)
Task IMultiplayerClient.UserVotedToSkipIntro(int userId, bool voted)
{
handleRoomRequest(() =>
{
@@ -940,9 +936,8 @@ namespace osu.Game.Online.Multiplayer
if (user == null)
return;
user.VotedToSkipIntro = true;
UserVotedToSkipIntro?.Invoke(userId);
user.VotedToSkipIntro = voted;
UserVotedToSkipIntro?.Invoke(userId, voted);
});
return Task.CompletedTask;
@@ -1117,7 +1112,7 @@ namespace osu.Game.Online.Multiplayer
Task IMatchmakingClient.MatchmakingItemSelected(int userId, long playlistItemId)
{
Scheduler.Add(() =>
handleRoomRequest(() =>
{
MatchmakingItemSelected?.Invoke(userId, playlistItemId);
RoomUpdated?.Invoke();
@@ -1128,7 +1123,7 @@ namespace osu.Game.Online.Multiplayer
Task IMatchmakingClient.MatchmakingItemDeselected(int userId, long playlistItemId)
{
Scheduler.Add(() =>
handleRoomRequest(() =>
{
MatchmakingItemDeselected?.Invoke(userId, playlistItemId);
RoomUpdated?.Invoke();

View File

@@ -70,7 +70,7 @@ namespace osu.Game.Online.Multiplayer
connection.On<MultiplayerPlaylistItem>(nameof(IMultiplayerClient.PlaylistItemAdded), ((IMultiplayerClient)this).PlaylistItemAdded);
connection.On<long>(nameof(IMultiplayerClient.PlaylistItemRemoved), ((IMultiplayerClient)this).PlaylistItemRemoved);
connection.On<MultiplayerPlaylistItem>(nameof(IMultiplayerClient.PlaylistItemChanged), ((IMultiplayerClient)this).PlaylistItemChanged);
connection.On<int>(nameof(IMultiplayerClient.UserVotedToSkipIntro), ((IMultiplayerClient)this).UserVotedToSkipIntro);
connection.On<int, bool>(nameof(IMultiplayerClient.UserVotedToSkipIntro), ((IMultiplayerClient)this).UserVotedToSkipIntro);
connection.On(nameof(IMultiplayerClient.VoteToSkipIntroPassed), ((IMultiplayerClient)this).VoteToSkipIntroPassed);
connection.On(nameof(IMatchmakingClient.MatchmakingQueueJoined), ((IMatchmakingClient)this).MatchmakingQueueJoined);

View File

@@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
resetPlaceholderText();
TextBox.HoldFocus = false;
TextBox.ReleaseFocusOnCommit = true;
TextBox.ReleaseFocusOnCommit = false;
TextBox.Focus = () => TextBox.PlaceholderText = ChatStrings.InputPlaceholder;
TextBox.FocusLost = resetPlaceholderText;

View File

@@ -520,6 +520,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
private void onBeatmapAvailabilityChanged(MultiplayerRoomUser user, BeatmapAvailability availability) => Scheduler.Add(() =>
{
if (!user.Equals(RoomUser))
return;
if (availability.State == DownloadState.Downloading)
downloadProgressBar.FadeIn(200, Easing.OutPow10);
else

View File

@@ -23,6 +23,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults
{
@@ -31,8 +32,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults
/// </summary>
public partial class SubScreenRoundResults : MatchmakingSubScreen
{
private const int panel_spacing = 5;
public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Hidden;
public override Drawable? PlayersDisplayArea => null;
@@ -51,7 +50,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
private AutoScrollContainer scrollContainer = null!;
private PanelContainer panelContainer = null!;
private LoadingSpinner loadingSpinner = null!;
[BackgroundDependencyLoader]
@@ -59,7 +58,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults
{
InternalChildren = new Drawable[]
{
scrollContainer = new AutoScrollContainer
panelContainer = new PanelContainer
{
RelativeSizeAxes = Axes.Both
},
@@ -136,78 +135,57 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults
private void setScores(ScoreInfo[] scores) => Scheduler.Add(() =>
{
Container panels;
scrollContainer.Child = panels = new Container
panelContainer.ChildrenEnumerable = scores.Select(s => new RoundResultsScorePanel(s)
{
RelativeSizeAxes = Axes.Y,
Width = scores.Length * (ScorePanel.CONTRACTED_WIDTH + panel_spacing),
ChildrenEnumerable = scores.Select(s => new RoundResultsScorePanel(s)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
})
};
for (int i = 0; i < panels.Count; i++)
{
panels[i].MoveToX(panels.DrawWidth * 2)
.Delay(i * 100)
.MoveToX((ScorePanel.CONTRACTED_WIDTH + panel_spacing) * i, 500, Easing.OutQuint);
}
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
});
private partial class RoundResultsScorePanel : CompositeDrawable
{
public RoundResultsScorePanel(ScoreInfo score)
{
AutoSizeAxes = Axes.Both;
InternalChild = new InstantSizingScorePanel(score);
Size = new Vector2(ScorePanel.CONTRACTED_WIDTH, ScorePanel.CONTRACTED_HEIGHT);
InternalChild = new ScorePanel(score);
}
public override bool PropagateNonPositionalInputSubTree => false;
public override bool PropagatePositionalInputSubTree => false;
private partial class InstantSizingScorePanel : ScorePanel
{
public InstantSizingScorePanel(ScoreInfo score, bool isNewLocalScore = false)
: base(score, isNewLocalScore)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
FinishTransforms(true);
}
}
}
private partial class AutoScrollContainer : UserTrackingScrollContainer
private partial class PanelContainer : Container<RoundResultsScorePanel>
{
private const float initial_offset = -0.5f;
private const double scroll_duration = 20000;
protected override Container<RoundResultsScorePanel> Content => flowContainer;
private double? scrollStartTime;
private readonly Container centreingContainer;
private readonly Container<RoundResultsScorePanel> flowContainer;
public AutoScrollContainer()
: base(Direction.Horizontal)
public PanelContainer()
{
InternalChild = new OsuScrollContainer(Direction.Horizontal)
{
RelativeSizeAxes = Axes.Both,
Child = centreingContainer = new Container
{
RelativeSizeAxes = Axes.Y,
Child = flowContainer = new FillFlowContainer<RoundResultsScorePanel>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Spacing = new Vector2(5)
}
}
};
}
protected override void Update()
{
base.Update();
if (!UserScrolling && Children.Count > 0)
{
scrollStartTime ??= Time.Current;
double scrollOffset = (Time.Current - scrollStartTime.Value) / scroll_duration;
if (scrollOffset < 1)
ScrollTo(DrawWidth * (initial_offset + scrollOffset), false);
}
centreingContainer.Width = Math.Max(DrawWidth, flowContainer.DrawWidth);
}
}
}

View File

@@ -68,7 +68,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state) => Schedule(updateCount);
private void onUserVotedToSkipIntro(int userId) => Schedule(() =>
private void onUserVotedToSkipIntro(int userId, bool voted) => Schedule(() =>
{
FadingContent.TriggerShow();
updateCount();

View File

@@ -37,7 +37,7 @@ namespace osu.Game.Screens.Ranking
/// <summary>
/// Height of the panel when contracted.
/// </summary>
private const float contracted_height = 385;
public const float CONTRACTED_HEIGHT = 385;
/// <summary>
/// Width of the panel when expanded.
@@ -280,7 +280,7 @@ namespace osu.Game.Screens.Ranking
break;
case PanelState.Contracted:
Size = new Vector2(CONTRACTED_WIDTH, contracted_height);
Size = new Vector2(CONTRACTED_WIDTH, CONTRACTED_HEIGHT);
topLayerBackground.FadeColour(getColour(contracted_top_layer_colour), RESIZE_DURATION, Easing.OutQuint);
middleLayerBackground.FadeColour(getColour(contracted_middle_layer_colour), RESIZE_DURATION, Easing.OutQuint);

View File

@@ -568,7 +568,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public async Task UserVoteToSkipIntro(int userId)
{
await ((IMultiplayerClient)this).UserVotedToSkipIntro(userId).ConfigureAwait(false);
await ((IMultiplayerClient)this).UserVotedToSkipIntro(userId, true).ConfigureAwait(false);
}
protected override Task<MultiplayerRoom> CreateRoomInternal(MultiplayerRoom room)

View File

@@ -2,10 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -17,6 +21,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Rulesets;
@@ -219,20 +224,35 @@ namespace osu.Game.Utils
}
}
private static readonly HashSet<int> ignored_io_exception_hresults =
[
// see https://stackoverflow.com/a/9294382 for how these are synthesised
unchecked((int)0x80070020), // ERROR_SHARING_VIOLATION
unchecked((int)0x80070027), // ERROR_HANDLE_DISK_FULL
unchecked((int)0x80070070), // ERROR_DISK_FULL
];
private bool shouldSubmitException(Exception exception)
{
switch (exception)
{
case IOException ioe:
// disk full exceptions, see https://stackoverflow.com/a/9294382
const int hr_error_handle_disk_full = unchecked((int)0x80070027);
const int hr_error_disk_full = unchecked((int)0x80070070);
// disk I/O failures, invalid formats, etc.
if (ioe.HResult == hr_error_handle_disk_full || ioe.HResult == hr_error_disk_full)
case IOException ioe:
if (ignored_io_exception_hresults.Contains(ioe.HResult))
return false;
break;
case UnauthorizedAccessException:
case SharpCompress.Common.InvalidFormatException:
return false;
// connectivity failures
case TimeoutException te:
return !te.Message.Contains(@"elapsed without receiving a message from the server");
case WebException we:
switch (we.Status)
{
@@ -242,6 +262,16 @@ namespace osu.Game.Utils
}
break;
case WebSocketException:
case SocketException:
return false;
// stuff that should really never make it to sentry
case APIAccess.WebRequestFlushedException:
case TaskCanceledException:
return false;
}
return true;

View File

@@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2025.1205.1" />
<PackageReference Include="ppy.osu.Framework" Version="2025.1209.0" />
<PackageReference Include="jvnkosu.Resources" Version="2025.1119.0" />
<PackageReference Include="Sentry" Version="5.1.1" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

@@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.1205.1" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.1209.0" />
</ItemGroup>
</Project>