19 Commits

Author SHA1 Message Date
fb7ccba4ba bump to 2025.1209.0-lazer 2025-12-09 20:18:13 +03:00
Dean Herbert
42b184f167 Update framework 2025-12-09 19:20:29 +09:00
Dean Herbert
27737bd4e9 Merge pull request #35938 from bdach/less-sentry
Filter out more exceptions from being sent to sentry
2025-12-09 19:10:57 +09:00
Bartłomiej Dach
8bb885a0dc 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 09:54:46 +01:00
Bartłomiej Dach
0b06acb29d Merge pull request #35909 from smoogipoo/fix-vote-to-skip
Adjust vote-to-skip messaging flow to be explicit about states
2025-12-09 08:59:53 +01:00
Bartłomiej Dach
b129837e57 Merge pull request #35918 from smoogipoo/qp-fix-unsafe-schedules
Fix potentially unsafe quick play event handling
2025-12-09 08:32:56 +01:00
Dean Herbert
3e221c7f61 Merge pull request #35906 from Natelytle/jank-tames-raiko
Rank the taiko swap mod
2025-12-09 15:57:02 +09:00
Dean Herbert
7106a6a5e5 Merge pull request #35897 from smoogipoo/qp-fix-download-progress
Fix incorrect quick play download progress
2025-12-09 15:53:15 +09:00
Dean Herbert
eaf2721f5b Merge pull request #35912 from smoogipoo/qp-remove-results-scroll
Remove quick play results scroll animation
2025-12-09 15:52:14 +09:00
Dean Herbert
59a27dad3d Merge pull request #35923 from smoogipoo/qp-abandoned-at
Consider abandon time for user placements
2025-12-08 19:27:56 +09:00
Dan Balasescu
d6cd748d2a Consider abandon time for user placements 2025-12-07 23:26:40 +09:00
Dan Balasescu
c23d6b7fd1 Fix potentially unsafe quick play event handling 2025-12-07 02:11:26 +09:00
Dean Herbert
582ff999aa Merge pull request #35913 from smoogipoo/qp-chat-hold-focus
Make quick play chat retain focus after posting
2025-12-06 20:20:42 +09:00
Dan Balasescu
a96b024ac5 Make quick play chat retain focus after posting 2025-12-06 17:50:58 +09:00
Dan Balasescu
1c10acba76 Allow score panel to animate 2025-12-06 17:40:17 +09:00
Dan Balasescu
4ae4c700ae Remove quick play round results scroll animation 2025-12-06 17:39:54 +09:00
Dan Balasescu
2be50d917a Adjust vote-to-skip to be explicit about states 2025-12-06 13:23:16 +09:00
Natelytle
35fdc6f8b9 Rank swap mod 2025-12-05 22:51:00 -05:00
Dan Balasescu
d04029bcc7 Fix incorrect quick play download progress 2025-12-06 03:24:17 +09:00
19 changed files with 171 additions and 109 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()
private partial class PanelContainer : Container<RoundResultsScorePanel>
{
base.LoadComplete();
FinishTransforms(true);
protected override Container<RoundResultsScorePanel> Content => flowContainer;
private readonly Container centreingContainer;
private readonly Container<RoundResultsScorePanel> flowContainer;
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)
}
}
}
private partial class AutoScrollContainer : UserTrackingScrollContainer
{
private const float initial_offset = -0.5f;
private const double scroll_duration = 20000;
private double? scrollStartTime;
public AutoScrollContainer()
: base(Direction.Horizontal)
{
};
}
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

@@ -36,6 +36,7 @@
</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>