diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 3cf95b3304a2..c9532de62036 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -48,7 +48,7 @@ private partial class TestLeaderboard : MultiplayerLeaderboardProvider public Dictionary> UserMods => UserScores.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ScoreProcessor.Mods); public TestLeaderboard(MultiplayerRoomUser[] users) - : base(users) + : base(users, false) { } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index 6e5add006ae4..edffe44ae80a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -26,7 +26,7 @@ protected override MultiplayerRoomUser CreateUser(int userId) } protected override MultiplayerLeaderboardProvider CreateLeaderboardProvider() => - new MultiplayerLeaderboardProvider(MultiplayerUsers.ToArray()) + new MultiplayerLeaderboardProvider(MultiplayerUsers.ToArray(), false) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayUserDisplay.cs b/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayUserDisplay.cs index b0ca297e3547..f8d08348877f 100644 --- a/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayUserDisplay.cs +++ b/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayUserDisplay.cs @@ -5,8 +5,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay; using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components; using osu.Game.Tests.Visual.Multiplayer; @@ -90,5 +92,19 @@ public void TestLastStand() health.Value = 1; }); } + + [Test] + public void TestMods() + { + AddStep("set user mods", () => MultiplayerClient.ChangeUserMods(1001, [new APIMod(new OsuModHidden())])); + + AddStep("reverse orientation", () => Child = new RankedPlayUserDisplay(new APIUser { Id = 1001, Username = "User 1001" }, Anchor.BottomRight, RankedPlayColourScheme.BLUE) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(256, 72), + Health = { BindTarget = health } + }); + } } } diff --git a/osu.Game/Online/Matchmaking/IMatchmakingServer.cs b/osu.Game/Online/Matchmaking/IMatchmakingServer.cs index 9127251b77d2..979de8369dcc 100644 --- a/osu.Game/Online/Matchmaking/IMatchmakingServer.cs +++ b/osu.Game/Online/Matchmaking/IMatchmakingServer.cs @@ -28,7 +28,7 @@ public interface IMatchmakingServer /// /// Joins the matchmaking queue, allowing the local user to get matched up with others. /// - Task MatchmakingJoinQueue(int poolId); + Task MatchmakingJoinQueueWithParams(MatchmakingJoinQueueRequest request); /// /// Leaves the matchmaking queue. diff --git a/osu.Game/Online/Matchmaking/Requests/MatchmakingJoinQueueRequest.cs b/osu.Game/Online/Matchmaking/Requests/MatchmakingJoinQueueRequest.cs new file mode 100644 index 000000000000..9943d755de2c --- /dev/null +++ b/osu.Game/Online/Matchmaking/Requests/MatchmakingJoinQueueRequest.cs @@ -0,0 +1,20 @@ +// 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 MessagePack; +using osu.Game.Online.API; + +namespace osu.Game.Online.Matchmaking.Requests +{ + [MessagePackObject] + [Serializable] + public class MatchmakingJoinQueueRequest + { + [Key(0)] + public int PoolId { get; set; } + + [Key(1)] + public APIMod[] Mods { get; set; } = []; + } +} diff --git a/osu.Game/Online/Matchmaking/Responses/MatchmakingJoinQueueResponse.cs b/osu.Game/Online/Matchmaking/Responses/MatchmakingJoinQueueResponse.cs new file mode 100644 index 000000000000..4d07557659c8 --- /dev/null +++ b/osu.Game/Online/Matchmaking/Responses/MatchmakingJoinQueueResponse.cs @@ -0,0 +1,14 @@ +// 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 MessagePack; + +namespace osu.Game.Online.Matchmaking.Responses +{ + [MessagePackObject] + [Serializable] + public class MatchmakingJoinQueueResponse + { + } +} diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 7f2cb69e1f74..bb3924e7a165 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -1210,7 +1210,7 @@ public RankedPlayCardWithPlaylistItem GetCardWithPlaylistItem(RankedPlayCardItem public abstract Task MatchmakingLeaveLobby(); - public abstract Task MatchmakingJoinQueue(int poolId); + public abstract Task MatchmakingJoinQueueWithParams(MatchmakingJoinQueueRequest request); public abstract Task MatchmakingLeaveQueue(); diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index fedddce45b3d..891358be5da6 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -383,13 +383,13 @@ public override Task MatchmakingLeaveLobby() return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingLeaveLobby)); } - public override Task MatchmakingJoinQueue(int poolId) + public override Task MatchmakingJoinQueueWithParams(MatchmakingJoinQueueRequest request) { if (!IsConnected.Value) - return Task.CompletedTask; + return Task.FromResult(new MatchmakingJoinQueueResponse()); Debug.Assert(connection != null); - return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingJoinQueue), poolId); + return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingJoinQueueWithParams), request); } public override Task MatchmakingLeaveQueue() diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index 74eaea8dbca0..fe7f9488b0a2 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -34,6 +34,9 @@ public class MultiplayerScore [JsonProperty("total_score")] public long TotalScore { get; set; } + [JsonProperty("total_score_without_mods")] + public long TotalScoreWithoutMods { get; set; } + [JsonProperty("accuracy")] public double Accuracy { get; set; } diff --git a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs index 7da0b4f279c6..826c6936d2b5 100644 --- a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs +++ b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs @@ -14,7 +14,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Scoring.Legacy; namespace osu.Game.Online.Spectator { @@ -24,11 +23,24 @@ namespace osu.Game.Online.Spectator /// public partial class SpectatorScoreProcessor : Component { + /// + /// Whether to use the the total score without mods for display purposes. + /// + public bool UseTotalScoreWithoutMods { get; set; } + /// /// The current total score. /// public readonly BindableLong TotalScore = new BindableLong { MinValue = 0 }; + /// + /// The total number of points awarded for the score without including mod multipliers. + /// + /// + /// The purpose of this property is to enable future lossless rebalances of mod multipliers. + /// + public readonly BindableLong TotalScoreWithoutMods = new BindableLong { MinValue = 0 }; + /// /// The current accuracy. /// @@ -52,9 +64,12 @@ public partial class SpectatorScoreProcessor : Component /// /// The applied s. /// - public IReadOnlyList Mods => scoreInfo?.Mods ?? Array.Empty(); + public IReadOnlyList Mods => Score?.Mods ?? Array.Empty(); - public Func GetDisplayScore => mode => scoreInfo?.GetDisplayScore(mode) ?? 0; + /// + /// The score. + /// + public ScoreInfo? Score { get; private set; } private IClock? referenceClock; @@ -78,7 +93,6 @@ public IClock ReferenceClock private readonly int userId; private SpectatorState? spectatorState; - private ScoreInfo? scoreInfo; public SpectatorScoreProcessor(int userId) { @@ -101,13 +115,13 @@ private void onSpectatorStatesChanged(object? sender, NotifyDictionaryChangedEve { if (!spectatorStates.TryGetValue(userId, out var userState) || userState.BeatmapID == null || userState.RulesetID == null) { - scoreInfo = null; + Score = null; spectatorState = null; replayFrames.Clear(); return; } - if (scoreInfo != null) + if (Score != null) return; RulesetInfo? rulesetInfo = rulesetStore.GetRuleset(userState.RulesetID.Value); @@ -117,7 +131,7 @@ private void onSpectatorStatesChanged(object? sender, NotifyDictionaryChangedEve Ruleset ruleset = rulesetInfo.CreateInstance(); spectatorState = userState; - scoreInfo = new ScoreInfo + Score = new ScoreInfo { Ruleset = rulesetInfo, Mods = userState.Mods.Select(m => m.ToMod(ruleset)).ToArray() @@ -131,7 +145,7 @@ private void onNewFrames(int incomingUserId, FrameDataBundle bundle) Schedule(() => { - if (scoreInfo == null) + if (Score == null) return; replayFrames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header)); @@ -141,7 +155,7 @@ private void onNewFrames(int incomingUserId, FrameDataBundle bundle) public void UpdateScore() { - if (scoreInfo == null || replayFrames.Count == 0) + if (Score == null || replayFrames.Count == 0) return; Debug.Assert(spectatorState != null); @@ -154,16 +168,18 @@ public void UpdateScore() TimedFrame frame = replayFrames[frameIndex]; Debug.Assert(frame.Header != null); - scoreInfo.Accuracy = frame.Header.Accuracy; - scoreInfo.MaxCombo = frame.Header.MaxCombo; - scoreInfo.Statistics = frame.Header.Statistics; - scoreInfo.MaximumStatistics = spectatorState.MaximumStatistics; - scoreInfo.TotalScore = frame.Header.TotalScore; + Score.Accuracy = frame.Header.Accuracy; + Score.MaxCombo = frame.Header.MaxCombo; + Score.Statistics = frame.Header.Statistics; + Score.MaximumStatistics = spectatorState.MaximumStatistics; + Score.TotalScore = frame.Header.TotalScore; + Score.TotalScoreWithoutMods = frame.Header.TotalScoreWithoutMods ?? 0; Accuracy.Value = frame.Header.Accuracy; Combo.Value = frame.Header.Combo; HighestCombo.Value = frame.Header.MaxCombo; TotalScore.Value = frame.Header.TotalScore; + TotalScoreWithoutMods.Value = frame.Header.TotalScoreWithoutMods ?? 0; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 32d58b29e1e6..d289263004ac 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -44,7 +44,12 @@ public partial class ScoreProcessor : JudgementProcessor /// /// Should only be disabled for special cases. /// When disabled, cannot be used. - internal bool TrackHitEvents = true; + internal bool TrackHitEvents { get; set; } = true; + + /// + /// Whether to use the the total score without mods for display purposes. + /// + internal bool UseTotalScoreWithoutMods { get; set; } /// /// Invoked when this was reset from a replay frame. diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index 664f1fd4ab00..8f1885a4e883 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; +using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring.Legacy @@ -13,18 +14,34 @@ namespace osu.Game.Scoring.Legacy public static class ScoreInfoExtensions { public static long GetDisplayScore(this ScoreProcessor scoreProcessor, ScoringMode mode) - => getDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics); + { + if (scoreProcessor.UseTotalScoreWithoutMods) + return GetDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScoreWithoutMods.Value, mode, scoreProcessor.MaximumStatistics); + + return GetDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics); + } + + public static long GetDisplayScore(this SpectatorScoreProcessor scoreProcessor, ScoringMode mode) + { + if (scoreProcessor.Score is not ScoreInfo score) + return 0; + + if (scoreProcessor.UseTotalScoreWithoutMods) + return GetDisplayScore(score.RulesetID, score.TotalScoreWithoutMods, mode, score.MaximumStatistics); + + return GetDisplayScore(score.RulesetID, score.TotalScore, mode, score.MaximumStatistics); + } public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode) - => getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics); + => GetDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics); public static long GetDisplayScore(this SoloScoreInfo soloScoreInfo, ScoringMode mode) - => getDisplayScore(soloScoreInfo.RulesetID, soloScoreInfo.TotalScore, mode, soloScoreInfo.MaximumStatistics); + => GetDisplayScore(soloScoreInfo.RulesetID, soloScoreInfo.TotalScore, mode, soloScoreInfo.MaximumStatistics); public static long GetDisplayScore(this MultiplayerScore multiplayerScore, ScoringMode mode) - => getDisplayScore(multiplayerScore.RulesetId, multiplayerScore.TotalScore, mode, multiplayerScore.MaximumStatistics); + => GetDisplayScore(multiplayerScore.RulesetId, multiplayerScore.TotalScore, mode, multiplayerScore.MaximumStatistics); - private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary maximumStatistics) + public static long GetDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary maximumStatistics) { if (mode == ScoringMode.Standardised) return score; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs index 8d9269722415..8bd5098a188e 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System.Diagnostics; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -15,6 +17,7 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Localisation; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Matchmaking; using osu.Game.Online.Matchmaking.Requests; @@ -22,6 +25,7 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Matchmaking.Intro; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue @@ -36,6 +40,7 @@ public partial class QueueController : Component { public readonly Bindable CurrentState = new Bindable(); public readonly Bindable SelectedPool = new Bindable(); + public readonly Bindable> SelectedMods = new Bindable>([]); /// /// Timer since the queue was joined. @@ -74,13 +79,18 @@ protected override void LoadComplete() /// Joins the matchmaking queue. /// /// The pool to join. - public void JoinQueue(MatchmakingPool pool) + /// The requested mods. + public void JoinQueue(MatchmakingPool pool, Mod[] mods) { lastDuelUser = null; lastDuelPool = null; QueueTimer.Restart(); - client.MatchmakingJoinQueue(pool.Id).FireAndForget(); + client.MatchmakingJoinQueueWithParams(new MatchmakingJoinQueueRequest + { + PoolId = pool.Id, + Mods = mods.Select(m => new APIMod(m)).ToArray() + }).FireAndForget(); } /// @@ -129,7 +139,7 @@ public void RejoinQueue() if (lastDuelUser != null && lastDuelPool != null) IssueDuel(lastDuelPool, lastDuelUser.Value); else if (SelectedPool.Value != null) - JoinQueue(SelectedPool.Value); + JoinQueue(SelectedPool.Value, SelectedMods.Value.ToArray()); } /// diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/RankedPlayFooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/RankedPlayFooterButtonFreeMods.cs new file mode 100644 index 000000000000..f6a856f2b4dd --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/RankedPlayFooterButtonFreeMods.cs @@ -0,0 +1,128 @@ +// 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 System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Footer; +using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue +{ + public partial class RankedPlayFooterButtonFreeMods : ScreenFooterButton + { + public new readonly IBindable State = new Bindable(); + + public readonly Bindable> Mods = new Bindable>([]); + + public new Action Action + { + set => throw new NotSupportedException("The click action is handled by the button itself."); + } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Container modsWedge = null!; + + public RankedPlayFooterButtonFreeMods(ModSelectOverlay overlay) + : base(overlay) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Text = OnlinePlayStrings.FooterButtonFreemods; + TooltipText = MultiplayerMatchStrings.FreeModsButtonTooltip; + Icon = FontAwesome.Solid.ExchangeAlt; + AccentColour = colours.Lime1; + + Add(modsWedge = new InputBlockingContainer + { + Y = -5f, + Depth = float.MaxValue, + Origin = Anchor.BottomLeft, + Shear = OsuGame.SHEAR, + CornerRadius = CORNER_RADIUS, + Size = new Vector2(BUTTON_WIDTH, FooterButtonMods.BAR_HEIGHT), + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 4, + // Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad. + Colour = Colour4.Black.Opacity(0.25f), + Offset = new Vector2(0, 2), + }, + Alpha = 0, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background4, + RelativeSizeAxes = Axes.Both, + }, + new Container + { + CornerRadius = CORNER_RADIUS, + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new ModDisplay(showExtendedInformation: true) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shear = -OsuGame.SHEAR, + Scale = new Vector2(0.5f), + Current = { BindTarget = Mods }, + ExpansionMode = ExpansionMode.AlwaysContracted, + }, + } + }, + } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + State.BindValueChanged(s => + { + if (s.NewValue == ScreenQueue.MatchmakingScreenState.Idle) + Enabled.Value = true; + else + { + Enabled.Value = false; + Overlay?.Hide(); + } + }, true); + + Mods.BindValueChanged(m => + { + if (m.NewValue.Count == 0) + modsWedge.FadeOut(300, Easing.OutExpo); + else + modsWedge.FadeIn(300, Easing.OutExpo); + }, true); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/RankedPlayModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/RankedPlayModSelectOverlay.cs new file mode 100644 index 000000000000..97adaa73015e --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/RankedPlayModSelectOverlay.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue +{ + public partial class RankedPlayModSelectOverlay : UserModSelectOverlay + { + public RankedPlayModSelectOverlay() + : base(OverlayColourScheme.Plum) + { + // Synchronise with server. + IsValidMod = m => + { + switch (m) + { + case ModHidden: + case ModTraceable: + return true; + + default: + return false; + } + }; + } + + public override VisibilityContainer CreateFooterContent() => new RankedPlayModSelectFooterContent(this) + { + Beatmap = { BindTarget = Beatmap }, + ActiveMods = { BindTarget = ActiveMods }, + Ruleset = { BindTarget = Ruleset }, + }; + + public partial class RankedPlayModSelectFooterContent : ModSelectFooterContent + { + protected override bool ShowModEffects => false; + + public RankedPlayModSelectFooterContent(RankedPlayModSelectOverlay overlay) + : base(overlay) + { + } + + protected override IEnumerable CreateButtons() => []; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs index 67632c55cd0b..0c9d3e772ba6 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs @@ -39,6 +39,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Volume; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Footer; using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay; @@ -56,6 +57,8 @@ public partial class ScreenQueue : OsuScreen public override bool? ApplyModTrackAdjustments => false; + public override bool DisallowExternalBeatmapRulesetChanges => true; + private Container mainContent = null!; private CloudVisualisation cloud = null!; private RatingDistributionGraph ratingGraph = null!; @@ -74,20 +77,26 @@ public partial class ScreenQueue : OsuScreen private UserLookupCache userLookupCache { get; set; } = null!; [Resolved] - private IBindable ruleset { get; set; } = null!; + private MusicController music { get; set; } = null!; [Resolved] - private MusicController music { get; set; } = null!; + private RulesetStore rulesets { get; set; } = null!; [Resolved] private DashboardOverlay? dashboardOverlay { get; set; } - private readonly IBindable currentState = new Bindable(); + [Resolved] + private IOverlayManager? overlayManager { get; set; } + private readonly IBindable currentState = new Bindable(); private readonly Bindable availablePools = new Bindable(); private readonly Bindable selectedPool = new Bindable(); + private readonly Bindable> selectedMods = new Bindable>(); + private readonly MatchmakingPoolType poolType; + private readonly RankedPlayModSelectOverlay modSelectOverlay; + private CancellationTokenSource userLookupCancellation = new CancellationTokenSource(); private Sample? enqueueSample; @@ -98,15 +107,20 @@ public partial class ScreenQueue : OsuScreen private DrawableSample waitingLoop = null!; private ScheduledDelegate? pushScreenDelegate; - private int? userRating; - private GridContainer mainGrid = null!; - private IBindable isConnected = null!; + private int? userRating; + private IDisposable? modSelectOverlayRegistration; + public ScreenQueue(MatchmakingPoolType poolType) { this.poolType = poolType; + + modSelectOverlay = new RankedPlayModSelectOverlay + { + SelectedMods = { BindTarget = selectedMods }, + }; } [BackgroundDependencyLoader] @@ -131,7 +145,7 @@ private void load(AudioManager audio, IAPIProvider api) { Horizontal = 20, Top = 20, - Bottom = ScreenFooter.HEIGHT + 20 + Bottom = ScreenFooter.HEIGHT + 50 }, RowDimensions = [ @@ -361,6 +375,8 @@ private void load(AudioManager audio, IAPIProvider api) experimentalText.AddText(" and provide any "); experimentalText.AddLink("feedback", @"https://osu.ppy.sh/community/forums/topics/2198397", sp => sp.Font = sp.Font.With(weight: FontWeight.SemiBold)); experimentalText.AddText(" on the osu! forums!"); + + LoadComponent(modSelectOverlay); } protected override void LoadComplete() @@ -387,7 +403,9 @@ protected override void LoadComplete() currentState.BindValueChanged(s => SetState(s.NewValue)); selectedPool.BindTo(queue.SelectedPool); - selectedPool.BindValueChanged(e => refreshLobbyData()); + selectedPool.BindValueChanged(onSelectedPoolChanged, true); + + selectedMods.BindTo(queue.SelectedMods); isConnected = client.IsConnected.GetBoundCopy(); isConnected.BindValueChanged(connected => Schedule(() => @@ -403,6 +421,19 @@ protected override void LoadComplete() clearLobbyData(); } }), true); + + modSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(modSelectOverlay); + } + + private void onSelectedPoolChanged(ValueChangedEvent e) + { + refreshLobbyData(); + + if (e.NewValue != null) + Ruleset.Value = rulesets.GetRuleset(e.NewValue.RulesetId); + + if (e.NewValue?.Equals(e.OldValue) != true) + selectedMods.Value = []; } private async Task populateAvailablePools() @@ -414,7 +445,7 @@ private async Task populateAvailablePools() availablePools.Value = pools; // Default to the currently queueing pool, or fallback to the user's ruleset for the initial pool selection. - selectedPool.Value ??= pools.FirstOrDefault(p => p.RulesetId == ruleset.Value.OnlineID) ?? pools.FirstOrDefault(); + selectedPool.Value ??= pools.FirstOrDefault(p => p.RulesetId == Ruleset.Value.OnlineID) ?? pools.FirstOrDefault(); }); } @@ -528,6 +559,8 @@ public override bool OnExiting(ScreenExitEvent e) if (base.OnExiting(e)) return true; + modSelectOverlay.Hide(); + stopWaitingLoopPlayback(); switch (currentState.Value) @@ -548,6 +581,17 @@ public override bool OnExiting(ScreenExitEvent e) } } + public override bool OnBackButton() + { + if (modSelectOverlay.State.Value == Visibility.Visible) + { + modSelectOverlay.Hide(); + return true; + } + + return base.OnBackButton(); + } + public void SetState(MatchmakingScreenState newState) { mainContent.FadeInFromZero(500, Easing.OutQuint); @@ -593,7 +637,7 @@ public void SetState(MatchmakingScreenState newState) Action = () => { Debug.Assert(selectedPool.Value != null); - queue.JoinQueue(selectedPool.Value); + queue.JoinQueue(selectedPool.Value, selectedMods.Value.ToArray()); }, Text = "Begin queueing", }, @@ -747,10 +791,21 @@ public void SetState(MatchmakingScreenState newState) } } + public override IReadOnlyList CreateFooterButtons() => + [ + new RankedPlayFooterButtonFreeMods(modSelectOverlay) + { + State = { BindTarget = currentState }, + Mods = { BindTarget = selectedMods } + } + ]; + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + modSelectOverlayRegistration?.Dispose(); + stopWaitingLoopPlayback(); if (client.IsNotNull()) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayUserDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayUserDisplay.cs index 9c5bfd3723e2..ade6e5256962 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayUserDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayUserDisplay.cs @@ -23,6 +23,8 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Screens.Play.HUD; using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; @@ -48,12 +50,16 @@ public partial class RankedPlayUserDisplay : CompositeDrawable private OsuSpriteText beatmapState = null!; private OsuSpriteText damageMultiplierText = null!; private OsuSpriteText lastStandText = null!; + private ModDisplay modDisplay = null!; private BeatmapAvailability availability = BeatmapAvailability.Unknown(); [Resolved] private MultiplayerClient client { get; set; } = null!; + [Resolved] + private RulesetStore rulesetStore { get; set; } = null!; + [Resolved] private RankedPlayCornerPiece? cornerPiece { get; set; } @@ -83,30 +89,47 @@ private void load() ? Anchor.CentreLeft : Anchor.CentreRight; + var modsAnchor = (contentAnchor & Anchor.x0) != 0 + ? Anchor.TopCentre + : Anchor.BottomCentre; + InternalChildren = [ - new CircularContainer + new Container { - Name = "Avatar", Size = new Vector2(72), - Masking = true, Anchor = contentAnchor, Origin = contentAnchor, Children = [ - new Box + new CircularContainer { + Name = "Avatar", RelativeSizeAxes = Axes.Both, - Colour = colourScheme.Surface, - Alpha = 0.5f, + Masking = true, + Children = + [ + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourScheme.Surface, + Alpha = 0.5f, + }, + grayScaleContainer = new BufferedContainer(cachedFrameBuffer: false, pixelSnapping: true) + { + RelativeSizeAxes = Axes.Both, + Child = new UpdateableAvatar(user) + { + RelativeSizeAxes = Axes.Both, + } + } + ] }, - grayScaleContainer = new BufferedContainer(cachedFrameBuffer: false, pixelSnapping: true) + modDisplay = new ModDisplay(false) { - RelativeSizeAxes = Axes.Both, - Child = new UpdateableAvatar(user) - { - RelativeSizeAxes = Axes.Both, - } + Anchor = modsAnchor, + Origin = Anchor.Centre, + Scale = new Vector2(0.5f) } ] }, @@ -198,7 +221,12 @@ protected override void LoadComplete() }); client.RoomUpdated += onRoomUpdated; + client.UserModsChanged += onUserModsChanged; + onRoomUpdated(); + + foreach (var roomUser in client.Room?.Users ?? []) + onUserModsChanged(roomUser); } private void onRoomUpdated() @@ -207,6 +235,15 @@ private void onRoomUpdated() updateDamageMultiplier(); } + private void onUserModsChanged(MultiplayerRoomUser roomUser) + { + if (roomUser.UserID != user.Id) + return; + + Ruleset ruleset = rulesetStore.GetRuleset(client.Room!.CurrentPlaylistItem.RulesetID)!.CreateInstance(); + modDisplay.Current.Value = roomUser.Mods.Select(m => m.ToMod(ruleset)).ToArray(); + } + private void updateBeatmapState() { var multiplayerUser = client.Room?.Users.SingleOrDefault(u => u.UserID == user.Id); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs index 2ea154eeae42..ca79d6111885 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs @@ -164,7 +164,7 @@ protected override void LoadComplete() globalBeatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); globalRuleset.Value = ruleset; - globalMods.Value = item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + globalMods.Value = Client.LocalUser!.Mods.Concat(item.RequiredMods).Select(m => m.ToMod(rulesetInstance)).ToArray(); // Play the new track from its preview point. globalBeatmap.Value.PrepareTrackForPreview(false); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayPlayer.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayPlayer.cs new file mode 100644 index 000000000000..21dfd37b850a --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayPlayer.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Screens; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Multiplayer; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay +{ + public partial class RankedPlayPlayer : MultiplayerPlayer + { + public RankedPlayPlayer(MultiplayerRoom room) + : base(new Room(room), new PlaylistItem(room.CurrentPlaylistItem), room.Users.ToArray(), showFailingOverlay: false, useTotalScoreWithoutMods: true) + { + } + + protected override async Task PrepareScoreForResultsAsync(Score score) + { + await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); + + Scheduler.Add(() => + { + if (this.IsCurrentScreen()) + this.Exit(); + }); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs index 7feab8e5b89f..ab622539063f 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs @@ -22,13 +22,11 @@ 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.Overlays.Dialog; using osu.Game.Overlays.Volume; using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Components; -using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Gameplay; using osu.Game.Screens.OnlinePlay.Matchmaking.Queue; using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card; using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components; @@ -300,7 +298,7 @@ private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState s private void onLoadRequested() => Scheduler.Add(() => { sampleStart?.Play(); - this.Push(new MultiplayerPlayerLoader(() => new ScreenGameplay(new Room(room), new PlaylistItem(client.Room!.CurrentPlaylistItem), room.Users.ToArray()))); + this.Push(new MultiplayerPlayerLoader(() => new RankedPlayPlayer(room))); }); private void onStageChanged(RankedPlayStage stage) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 66e98d1c322e..8157e067577e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -52,17 +52,19 @@ public partial class MultiplayerPlayer : RoomSubmittingPlayer /// The playlist item to be played. /// The users which are participating in this game. /// Whether to show the red failing overlay. - public MultiplayerPlayer(Room room, PlaylistItem playlistItem, MultiplayerRoomUser[] users, bool showFailingOverlay = true) + /// Whether to use the the total score without mods for display purposes. + public MultiplayerPlayer(Room room, PlaylistItem playlistItem, MultiplayerRoomUser[] users, bool showFailingOverlay = true, bool useTotalScoreWithoutMods = false) : base(room, playlistItem, new PlayerConfiguration { AllowPause = false, AllowRestart = false, AutomaticallySkipIntro = room.AutoSkip, ShowLeaderboard = true, - ShowFailingOverlay = showFailingOverlay + ShowFailingOverlay = showFailingOverlay, + UseTotalScoreWithoutMods = useTotalScoreWithoutMods }) { - leaderboardProvider = new MultiplayerLeaderboardProvider(users); + leaderboardProvider = new MultiplayerLeaderboardProvider(users, useTotalScoreWithoutMods); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Play/Leaderboards/MultiSpectatorLeaderboardProvider.cs b/osu.Game/Screens/Play/Leaderboards/MultiSpectatorLeaderboardProvider.cs index a6f75e6ca14d..c9baf5661dd6 100644 --- a/osu.Game/Screens/Play/Leaderboards/MultiSpectatorLeaderboardProvider.cs +++ b/osu.Game/Screens/Play/Leaderboards/MultiSpectatorLeaderboardProvider.cs @@ -10,7 +10,7 @@ namespace osu.Game.Screens.Play.Leaderboards public partial class MultiSpectatorLeaderboardProvider : MultiplayerLeaderboardProvider { public MultiSpectatorLeaderboardProvider(MultiplayerRoomUser[] users) - : base(users) + : base(users, false) { } diff --git a/osu.Game/Screens/Play/Leaderboards/MultiplayerLeaderboardProvider.cs b/osu.Game/Screens/Play/Leaderboards/MultiplayerLeaderboardProvider.cs index 4d3485ba2168..be21dabe0933 100644 --- a/osu.Game/Screens/Play/Leaderboards/MultiplayerLeaderboardProvider.cs +++ b/osu.Game/Screens/Play/Leaderboards/MultiplayerLeaderboardProvider.cs @@ -38,6 +38,7 @@ public partial class MultiplayerLeaderboardProvider : CompositeComponent, IGamep public bool HasTeams => TeamScores.Count > 0; private readonly MultiplayerRoomUser[] users; + private readonly bool useTotalScoreWithoutMods; private readonly Bindable scoringMode = new Bindable(); private readonly IBindableList playingUserIds = new BindableList(); @@ -56,9 +57,10 @@ public partial class MultiplayerLeaderboardProvider : CompositeComponent, IGamep private readonly Cached sorting = new Cached(); - public MultiplayerLeaderboardProvider(MultiplayerRoomUser[] users) + public MultiplayerLeaderboardProvider(MultiplayerRoomUser[] users, bool useTotalScoreWithoutMods) { this.users = users; + this.useTotalScoreWithoutMods = useTotalScoreWithoutMods; } [BackgroundDependencyLoader] @@ -69,6 +71,7 @@ private void load(OsuConfigManager config, IAPIProvider api, CancellationToken c foreach (var user in users) { var scoreProcessor = new SpectatorScoreProcessor(user.UserID); + scoreProcessor.UseTotalScoreWithoutMods = useTotalScoreWithoutMods; scoreProcessor.Mode.BindTo(scoringMode); scoreProcessor.TotalScore.BindValueChanged(_ => Scheduler.AddOnce(updateTotals)); AddInternal(scoreProcessor); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 77f4a6a3da15..2fa5c3756689 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -258,6 +258,7 @@ private void load(OsuConfigManager config, OsuGameBase game, CancellationToken c dependencies.CacheAs(scrollingRuleset.ScrollingInfo); ScoreProcessor = ruleset.CreateScoreProcessor(); + ScoreProcessor.UseTotalScoreWithoutMods = Configuration.UseTotalScoreWithoutMods; ScoreProcessor.Mods.Value = gameplayMods; ScoreProcessor.ApplyBeatmap(playableBeatmap); diff --git a/osu.Game/Screens/Play/PlayerConfiguration.cs b/osu.Game/Screens/Play/PlayerConfiguration.cs index 00a565397098..e48030d1117a 100644 --- a/osu.Game/Screens/Play/PlayerConfiguration.cs +++ b/osu.Game/Screens/Play/PlayerConfiguration.cs @@ -44,5 +44,10 @@ public class PlayerConfiguration /// Whether to show the red failing overlay. /// public bool ShowFailingOverlay { get; set; } = true; + + /// + /// Whether to use the the total score without mods for display purposes. + /// + public bool UseTotalScoreWithoutMods { get; set; } } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 77ce9d4b3c30..6be5e5524f5a 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -930,10 +930,12 @@ public override Task MatchmakingLeaveLobby() return Task.CompletedTask; } - public override async Task MatchmakingJoinQueue(int poolId) + public override async Task MatchmakingJoinQueueWithParams(MatchmakingJoinQueueRequest request) { await ((IMatchmakingClient)this).MatchmakingQueueJoined().ConfigureAwait(false); await ((IMatchmakingClient)this).MatchmakingQueueStatusChanged(new MatchmakingQueueStatus.Searching()).ConfigureAwait(false); + + return new MatchmakingJoinQueueResponse(); } public override async Task MatchmakingLeaveQueue()