diff --git a/SampleMultiplayerClient/MultiplayerClient.cs b/SampleMultiplayerClient/MultiplayerClient.cs index 7940ba2d..c19e63f6 100644 --- a/SampleMultiplayerClient/MultiplayerClient.cs +++ b/SampleMultiplayerClient/MultiplayerClient.cs @@ -309,7 +309,8 @@ public Task MatchmakingJoinLobbyWithParams(Matchma public Task MatchmakingLeaveLobby() => connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingLeaveLobby)); - public Task MatchmakingJoinQueue(int poolId) => connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingJoinQueue), poolId); + public Task MatchmakingJoinQueueWithParams(MatchmakingJoinQueueRequest request) + => connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingJoinQueueWithParams), request); public Task MatchmakingLeaveQueue() => connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingLeaveQueue)); diff --git a/SampleMultiplayerClient/Program.cs b/SampleMultiplayerClient/Program.cs index 71d49549..b91e9fff 100644 --- a/SampleMultiplayerClient/Program.cs +++ b/SampleMultiplayerClient/Program.cs @@ -142,7 +142,7 @@ public static async Task Main() break; case "mmjoinqueue": - await targetClient.MatchmakingJoinQueue(int.Parse(args[0])); + await targetClient.MatchmakingJoinQueueWithParams(new MatchmakingJoinQueueRequest { PoolId = int.Parse(args[0]) }); break; case "mmleavequeue": diff --git a/osu.Server.Spectator.Tests/Matchmaking/MatchmakingQueueBackgroundServiceTests.cs b/osu.Server.Spectator.Tests/Matchmaking/MatchmakingQueueBackgroundServiceTests.cs index 7fad302f..ca52e3c5 100644 --- a/osu.Server.Spectator.Tests/Matchmaking/MatchmakingQueueBackgroundServiceTests.cs +++ b/osu.Server.Spectator.Tests/Matchmaking/MatchmakingQueueBackgroundServiceTests.cs @@ -38,7 +38,7 @@ public MatchmakingQueueBackgroundServiceTests() [Fact] public async Task AddToQueue() { - await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!, 1); + await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!, 1, []); await MatchmakingBackgroundService.ExecuteOnceAsync(); UserReceiver.Verify(u => u.MatchmakingQueueJoined(), Times.Once); @@ -48,7 +48,7 @@ public async Task AddToQueue() [Fact] public async Task RemoveFromQueue() { - await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!, 1); + await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!, 1, []); await MatchmakingBackgroundService.RemoveFromQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!); await MatchmakingBackgroundService.ExecuteOnceAsync(); @@ -59,13 +59,13 @@ public async Task RemoveFromQueue() [Fact] public async Task MatchReady() { - await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!, 1); + await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!, 1, []); await MatchmakingBackgroundService.ExecuteOnceAsync(); UserReceiver.Verify(u => u.MatchmakingRoomInvitedWithParams(It.IsAny()), Times.Never); User2Receiver.Verify(u => u.MatchmakingRoomInvitedWithParams(It.IsAny()), Times.Never); - await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID_2)!, 1); + await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID_2)!, 1, []); await MatchmakingBackgroundService.ExecuteOnceAsync(); UserReceiver.Verify(u => u.MatchmakingRoomInvitedWithParams(It.IsAny()), Times.Once); @@ -75,8 +75,8 @@ public async Task MatchReady() [Fact] public async Task AcceptInvitation() { - await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!, 1); - await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID_2)!, 1); + await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!, 1, []); + await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID_2)!, 1, []); await MatchmakingBackgroundService.ExecuteOnceAsync(); UserReceiver.Verify(u => u.MatchmakingRoomInvitedWithParams(It.IsAny()), Times.Once); @@ -98,8 +98,8 @@ public async Task AcceptInvitation() [Fact] public async Task DeclineInvitation() { - await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!, 1); - await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID_2)!, 1); + await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!, 1, []); + await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID_2)!, 1, []); await MatchmakingBackgroundService.ExecuteOnceAsync(); UserReceiver.Verify(u => u.MatchmakingRoomInvitedWithParams(It.IsAny()), Times.Once); @@ -121,8 +121,8 @@ public async Task DeclineInvitation() [Fact] public async Task LeaveQueueAfterInvite() { - await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!, 1); - await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID_2)!, 1); + await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!, 1, []); + await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID_2)!, 1, []); await MatchmakingBackgroundService.ExecuteOnceAsync(); UserReceiver.Verify(u => u.MatchmakingRoomInvitedWithParams(It.IsAny()), Times.Once); @@ -146,8 +146,8 @@ public async Task LeaveQueueAfterInvite() [Fact] public async Task QueueLeftOnDisconnect() { - await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!, 1); - await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID_2)!, 1); + await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID)!, 1, []); + await MatchmakingBackgroundService.AddToQueueAsync(UserStates.GetEntityUnsafe(USER_ID_2)!, 1, []); await MatchmakingBackgroundService.ExecuteOnceAsync(); UserReceiver.Verify(u => u.MatchmakingRoomInvitedWithParams(It.IsAny()), Times.Once); diff --git a/osu.Server.Spectator.Tests/RankedPlay/RankedPlayMatchControllerTests.cs b/osu.Server.Spectator.Tests/RankedPlay/RankedPlayMatchControllerTests.cs new file mode 100644 index 00000000..fb2e14b2 --- /dev/null +++ b/osu.Server.Spectator.Tests/RankedPlay/RankedPlayMatchControllerTests.cs @@ -0,0 +1,137 @@ +// 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 System.Linq; +using System.Threading.Tasks; +using Moq; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay; +using osu.Server.Spectator.Database; +using osu.Server.Spectator.Database.Models; +using osu.Server.Spectator.Hubs.Multiplayer; +using osu.Server.Spectator.Hubs.Multiplayer.Matchmaking.Queue; +using osu.Server.Spectator.Hubs.Multiplayer.Matchmaking.RankedPlay; +using osu.Server.Spectator.Tests.Multiplayer; +using Xunit; + +namespace osu.Server.Spectator.Tests.RankedPlay +{ + public class RankedPlayMatchControllerTests : MultiplayerTest + { + protected ServerMultiplayerRoom Room { get; private set; } = null!; + protected RankedPlayMatchController MatchController => (RankedPlayMatchController)Room.MatchController; + + protected RankedPlayRoomState RoomState => (RankedPlayRoomState)Room.MatchState!; + protected RankedPlayUserInfo UserState => RoomState.Users[USER_ID]; + protected RankedPlayUserInfo User2State => RoomState.Users[USER_ID_2]; + + public RankedPlayMatchControllerTests() + { + AppSettings.MatchmakingRoomRounds = 2; + AppSettings.MatchmakingRoomAllowSkip = true; + + Database.Setup(db => db.GetRealtimeRoomAsync(ROOM_ID)) + .Callback(roomId => InitialiseRoom(roomId, 20)) + .ReturnsAsync(() => new multiplayer_room + { + type = database_match_type.ranked_play, + ends_at = DateTimeOffset.Now.AddMinutes(5), + user_id = int.Parse(Hub.Context.UserIdentifier!), + }); + + Database.Setup(db => db.GetMatchmakingUserStatsAsync(It.IsAny(), It.IsAny())) + .Returns((userId, poolId) => Task.FromResult(new matchmaking_user_stats + { + user_id = (uint)userId, + pool_id = poolId + })); + + Database.Setup(db => db.GetAllScoresForPlaylistItem(It.IsAny())) + .Returns(_ => Task.FromResult>( + [ + new SoloScore { user_id = USER_ID, total_score = 1_000_000 }, + new SoloScore { user_id = USER_ID_2, total_score = 1_000_000 }, + ])); + } + + [Fact] + public async Task UserModsAppliedOnEnter() + { + using (var room = await Rooms.GetForUse(ROOM_ID, true)) + { + room.Item = await ServerMultiplayerRoom.InitialiseMatchmakingRoomAsync + ( + ROOM_ID, + RoomController, + DatabaseFactory.Object, + EventDispatcher, + LoggerFactory.Object, + new matchmaking_pool(), + new[] + { + new MatchmakingQueueUser(USER_ID.ToString(), new[] { new APIMod { Acronym = "HD" } }) { UserId = USER_ID }, + new MatchmakingQueueUser(USER_ID_2.ToString()) { UserId = USER_ID_2 } + }, + new MatchmakingBeatmapSelector(new matchmaking_pool(), Enumerable.Range(1, 50).Select(i => new matchmaking_pool_beatmap + { + id = (uint)i, + beatmap_id = i + }).ToArray(), new Mock().Object), + new Mock().Object + ); + + Room = room.Item; + } + + await Hub.JoinRoom(ROOM_ID); + SetUserContext(ContextUser2); + await Hub.JoinRoom(ROOM_ID); + SetUserContext(ContextUser); + + Assert.Equal("HD", Room.Users.Single(u => u.UserID == USER_ID).Mods.Single().Acronym); + Assert.Empty(Room.Users.Single(u => u.UserID == USER_ID_2).Mods); + } + + [Fact] + public async Task UsersCanNotChangeOwnMods() + { + using (var room = await Rooms.GetForUse(ROOM_ID, true)) + { + room.Item = await ServerMultiplayerRoom.InitialiseMatchmakingRoomAsync + ( + ROOM_ID, + RoomController, + DatabaseFactory.Object, + EventDispatcher, + LoggerFactory.Object, + new matchmaking_pool(), + new[] + { + new MatchmakingQueueUser(USER_ID.ToString(), new[] { new APIMod { Acronym = "HD" } }) { UserId = USER_ID }, + new MatchmakingQueueUser(USER_ID_2.ToString()) { UserId = USER_ID_2 } + }, + new MatchmakingBeatmapSelector(new matchmaking_pool(), Enumerable.Range(1, 50).Select(i => new matchmaking_pool_beatmap + { + id = (uint)i, + beatmap_id = i + }).ToArray(), new Mock().Object), + new Mock().Object + ); + + Room = room.Item; + } + + await Hub.JoinRoom(ROOM_ID); + SetUserContext(ContextUser2); + await Hub.JoinRoom(ROOM_ID); + SetUserContext(ContextUser); + + Assert.Equal("HD", Room.Users.Single(u => u.UserID == USER_ID).Mods.Single().Acronym); + Assert.Empty(Room.Users.Single(u => u.UserID == USER_ID_2).Mods); + + await Assert.ThrowsAsync(() => Hub.ChangeUserMods([])); + } + } +} diff --git a/osu.Server.Spectator.Tests/RankedPlay/Stages/ResultsStageTests.cs b/osu.Server.Spectator.Tests/RankedPlay/Stages/ResultsStageTests.cs index 3f30eee4..0b583eea 100644 --- a/osu.Server.Spectator.Tests/RankedPlay/Stages/ResultsStageTests.cs +++ b/osu.Server.Spectator.Tests/RankedPlay/Stages/ResultsStageTests.cs @@ -458,6 +458,32 @@ public async Task MultipliersIncrease() Assert.Equal(0.5, User2State.DamageMultiplier); } + [Fact] + public async Task TotalScoreWithoutModsUsed() + { + Database.Setup(db => db.GetAllScoresForPlaylistItem(It.IsAny())) + .Returns(_ => Task.FromResult>( + [ + new SoloScore + { + user_id = USER_ID, + total_score = 1_000_000, + ScoreData = { TotalScoreWithoutMods = 500_000 } + }, + new SoloScore + { + user_id = USER_ID_2, + total_score = 0 + }, + ])); + + await MatchController.Stage.Enter(); + await FinishCountdown(); + + Assert.Equal(1_000_000, UserState.Life); + Assert.Equal(450_000, User2State.Life); + } + [Fact] public async Task UserNotKilledIfQuitInFinalRound() { diff --git a/osu.Server.Spectator/Database/Models/SoloScore.cs b/osu.Server.Spectator/Database/Models/SoloScore.cs index 1cabf495..6428216d 100644 --- a/osu.Server.Spectator/Database/Models/SoloScore.cs +++ b/osu.Server.Spectator/Database/Models/SoloScore.cs @@ -69,6 +69,7 @@ public string data BuildID = build_id, Passed = passed, TotalScore = total_score, + TotalScoreWithoutMods = ScoreData.TotalScoreWithoutMods ?? 0, Accuracy = accuracy, UserID = (int)user_id, MaxCombo = (int)max_combo, @@ -84,5 +85,7 @@ public string data PP = pp, HasReplay = has_replay }; + + public long TotalScoreWithoutMods => ScoreData.TotalScoreWithoutMods ?? total_score; } } diff --git a/osu.Server.Spectator/Database/Models/SoloScoreData.cs b/osu.Server.Spectator/Database/Models/SoloScoreData.cs index 2a7839fb..c6aa7537 100644 --- a/osu.Server.Spectator/Database/Models/SoloScoreData.cs +++ b/osu.Server.Spectator/Database/Models/SoloScoreData.cs @@ -20,5 +20,8 @@ public class SoloScoreData [JsonProperty("maximum_statistics")] public Dictionary MaximumStatistics { get; set; } = new Dictionary(); + + [JsonProperty("total_score_without_mods")] + public long? TotalScoreWithoutMods { get; set; } } } diff --git a/osu.Server.Spectator/Hubs/Multiplayer/IMatchController.cs b/osu.Server.Spectator/Hubs/Multiplayer/IMatchController.cs index 493fae29..f9f1bc9d 100644 --- a/osu.Server.Spectator/Hubs/Multiplayer/IMatchController.cs +++ b/osu.Server.Spectator/Hubs/Multiplayer/IMatchController.cs @@ -10,6 +10,11 @@ namespace osu.Server.Spectator.Hubs.Multiplayer { public interface IMatchController { + /// + /// Whether users should be able to change their own mods within the match. + /// + bool AllowUserModChanges { get; } + MultiplayerPlaylistItem CurrentItem { get; } Task Initialise(); diff --git a/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/MatchmakingMatchController.cs b/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/MatchmakingMatchController.cs index 19ac69b7..5b5cb528 100644 --- a/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/MatchmakingMatchController.cs +++ b/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/MatchmakingMatchController.cs @@ -96,6 +96,8 @@ public class MatchmakingMatchController : IMatchController, IMatchmakingMatchCon /// private static readonly int[] placement_points = [15, 12, 10, 8, 6, 4, 2, 1]; + public bool AllowUserModChanges => false; + public MultiplayerPlaylistItem CurrentItem => room.Playlist.Single(item => item.ID == room.Settings.PlaylistItemId); private readonly ServerMultiplayerRoom room; diff --git a/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/Queue/IMatchmakingQueueBackgroundService.cs b/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/Queue/IMatchmakingQueueBackgroundService.cs index b96f4626..57111067 100644 --- a/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/Queue/IMatchmakingQueueBackgroundService.cs +++ b/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/Queue/IMatchmakingQueueBackgroundService.cs @@ -47,7 +47,7 @@ public interface IMatchmakingQueueBackgroundService : IHostedService /// /// Adds a user to the matchmaking queue. /// - Task AddToQueueAsync(MultiplayerClientState state, int poolId); + Task AddToQueueAsync(MultiplayerClientState state, int poolId, APIMod[] mods); /// /// Removes a user from the matchmaking queue. diff --git a/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/Queue/MatchmakingQueueBackgroundService.cs b/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/Queue/MatchmakingQueueBackgroundService.cs index 6e3a9cd2..f251a38f 100644 --- a/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/Queue/MatchmakingQueueBackgroundService.cs +++ b/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/Queue/MatchmakingQueueBackgroundService.cs @@ -18,6 +18,7 @@ using osu.Game.Online.Matchmaking.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay; +using osu.Game.Utils; using osu.Server.Spectator.Database; using osu.Server.Spectator.Database.Models; using osu.Server.Spectator.Hubs.Multiplayer.Matchmaking.Elo; @@ -152,7 +153,7 @@ public async Task RemoveFromLobbyAsync(MultiplayerClientState state) await lobby.Remove(state); } - public async Task AddToQueueAsync(MultiplayerClientState state, int poolId) + public async Task AddToQueueAsync(MultiplayerClientState state, int poolId, APIMod[] mods) { // Users should only ever be in one queue at a time. await RemoveFromQueueAsync(state); @@ -164,8 +165,11 @@ public async Task AddToQueueAsync(MultiplayerClientState state, int poolId) if (!pool.active) throw new InvalidStateException("The selected matchmaking pool is no longer active."); + if (!ModUtils.InstantiateValidModsForRuleset(LegacyHelper.GetRulesetFromLegacyID(pool.ruleset_id), mods, out _)) + throw new InvalidStateException("Invalid mods selected for ruleset."); + MatchmakingQueue queue = poolQueues.GetOrAdd(poolId, _ => new MatchmakingQueue(pool)); - await processBundle(queue.Add(await createUserAsync(state, pool))); + await processBundle(queue.Add(await createUserAsync(state, pool, mods))); } } @@ -211,7 +215,7 @@ public async Task IssueDuelAsync(MultiplayerClient RequeueOnDecline = false }; - MatchmakingQueueUser user = await createUserAsync(state, pool); + MatchmakingQueueUser user = await createUserAsync(state, pool, []); user.BanEndTime = DateTimeOffset.MinValue; // The user is added to the queue before the queue is added to the dictionary @@ -253,7 +257,7 @@ public async Task AcceptDuelAsync(MultiplayerClie } // Add the user to the duel queue. - MatchmakingQueueUser user = await createUserAsync(state, queue.Pool); + MatchmakingQueueUser user = await createUserAsync(state, queue.Pool, []); user.BanEndTime = DateTimeOffset.MinValue; await processBundle(queue.Add(user)); @@ -516,7 +520,7 @@ private async Task processBundle(MatchmakingQueueUpdateBundle bundle) } } - private async Task createUserAsync(MultiplayerClientState state, matchmaking_pool pool) + private async Task createUserAsync(MultiplayerClientState state, matchmaking_pool pool, APIMod[] mods) { using (var db = databaseFactory.GetInstance()) { @@ -540,7 +544,7 @@ private async Task createUserAsync(MultiplayerClientState }); } - return new MatchmakingQueueUser(state.ConnectionId) + return new MatchmakingQueueUser(state.ConnectionId, mods) { UserId = state.UserId, Rating = stats.EloData.Rating, diff --git a/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/Queue/MatchmakingQueueUser.cs b/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/Queue/MatchmakingQueueUser.cs index 8497e75c..6f4520b3 100644 --- a/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/Queue/MatchmakingQueueUser.cs +++ b/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/Queue/MatchmakingQueueUser.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Game.Online.API; using osu.Server.Spectator.Hubs.Multiplayer.Matchmaking.Elo; namespace osu.Server.Spectator.Hubs.Multiplayer.Matchmaking.Queue @@ -51,9 +52,20 @@ public class MatchmakingQueueUser : IEquatable /// public readonly string Identifier; + /// + /// The mods with which this user queued. + /// + public readonly APIMod[] Mods; + public MatchmakingQueueUser(string identifier) + : this(identifier, []) + { + } + + public MatchmakingQueueUser(string identifier, APIMod[] mods) { Identifier = identifier; + Mods = mods; } public bool Equals(MatchmakingQueueUser? other) diff --git a/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/RankedPlay/RankedPlayMatchController.cs b/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/RankedPlay/RankedPlayMatchController.cs index fbcffd7c..bc0bd7fa 100644 --- a/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/RankedPlay/RankedPlayMatchController.cs +++ b/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/RankedPlay/RankedPlayMatchController.cs @@ -8,10 +8,14 @@ using Microsoft.AspNetCore.Mvc; using OpenSkillSharp.Models; using OpenSkillSharp.Rating; +using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay; using osu.Game.Online.RankedPlay; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; using osu.Server.Spectator.Database; using osu.Server.Spectator.Database.Models; using osu.Server.Spectator.Hubs.Multiplayer.Matchmaking.Elo; @@ -26,6 +30,8 @@ public class RankedPlayMatchController : IMatchController, IMatchmakingMatchCont public const int PLAYER_HAND_SIZE = 5; public const int DECK_SIZE = 50; + public bool AllowUserModChanges => false; + public MultiplayerPlaylistItem CurrentItem => Room.Playlist.Single(item => item.ID == Room.Settings.PlaylistItemId); public IMatchmakingQueueBackgroundService MatchmakingService { get; private set; } = null!; @@ -57,7 +63,9 @@ public class RankedPlayMatchController : IMatchController, IMatchmakingMatchCont /// public int[] UserIdsByTurnOrder { get; private set; } = []; - public Dictionary RatingByUser { get; private set; } = []; + public readonly Dictionary RatingByUser = []; + + private readonly Dictionary modsByUser = []; /// /// Mapping of cards to their associated effect. @@ -107,7 +115,7 @@ async Task IMatchmakingMatchController.Initialise(matchmaking_pool pool, Matchma foreach (var beatmap in beatmaps) { var card = new RankedPlayCardItem(); - cardToEffectMap[card] = beatmap.ToPlaylistItem(); + cardToEffectMap[card] = createPlaylistItem(beatmap); deck.Add(card); } @@ -116,7 +124,7 @@ async Task IMatchmakingMatchController.Initialise(matchmaking_pool pool, Matchma // Create an initial playlist item for the room. Clients require this to operate correctly. using (var db = DbFactory.GetInstance()) { - MultiplayerPlaylistItem initialItem = new MultiplayerPlaylistItem(); + MultiplayerPlaylistItem initialItem = createPlaylistItem(null); initialItem.ID = await db.AddPlaylistItemAsync(new multiplayer_playlist_item(Room.RoomID, initialItem)); Room.Playlist.Add(initialItem); @@ -127,6 +135,7 @@ async Task IMatchmakingMatchController.Initialise(matchmaking_pool pool, Matchma foreach (var user in users) { RatingByUser[user.UserId] = user.Rating; + modsByUser[user.UserId] = user.Mods; State.Users[user.UserId] = new RankedPlayUserInfo { Rating = (int)Math.Round(user.Rating.Mu), @@ -191,6 +200,9 @@ async Task IMatchController.HandleUserJoined(MultiplayerRoomUser user) { await EventDispatcher.PostPlayerJoinedMatchmakingRoomAsync(Room.RoomID, user.UserID); await Stage.HandleUserJoined(user); + + ModUtils.InstantiateValidModsForRuleset(LegacyHelper.GetRulesetFromLegacyID(Pool.ruleset_id), modsByUser[user.UserID], out List validMods); + await Room.ChangeUserMods(user.UserID, validMods.Where(isUserModAllowed).Select(m => new APIMod(m)).ToArray()); } async Task IMatchController.HandleUserLeft(MultiplayerRoomUser user) @@ -447,6 +459,30 @@ await db.InsertUserEloHistoryEntry( await MatchmakingService.RecordMatch((int)Pool.id, State); } + private MultiplayerPlaylistItem createPlaylistItem(matchmaking_pool_beatmap? beatmap) + { + var item = beatmap?.ToPlaylistItem() ?? new MultiplayerPlaylistItem(); + + Ruleset ruleset = LegacyHelper.GetRulesetFromLegacyID(Pool.ruleset_id); + item.AllowedMods = ruleset.AllMods.OfType().Where(isUserModAllowed).Select(m => new APIMod(m)).ToArray(); + + return item; + } + + private bool isUserModAllowed(IMod mod) + { + // Synchronise with client. + switch (mod) + { + case ModHidden: + case ModTraceable: + return true; + + default: + return false; + } + } + public MatchStartedEventDetail GetMatchDetails() => new MatchStartedEventDetail { room_type = database_match_type.ranked_play diff --git a/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/RankedPlay/Stages/ResultsStage.cs b/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/RankedPlay/Stages/ResultsStage.cs index 0ff4f0e0..75c8eaec 100644 --- a/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/RankedPlay/Stages/ResultsStage.cs +++ b/osu.Server.Spectator/Hubs/Multiplayer/Matchmaking/RankedPlay/Stages/ResultsStage.cs @@ -70,8 +70,8 @@ protected override async Task Begin() info.DamageInfo = Controller.Damage(userId); } - int winningTotalScore = (int)scores.Select(s => s.total_score).Max(); - SoloScore[] winningScores = scores.Where(u => u.total_score == winningTotalScore).ToArray(); + int winningTotalScore = (int)scores.Select(s => s.TotalScoreWithoutMods).Max(); + SoloScore[] winningScores = scores.Where(u => u.TotalScoreWithoutMods == winningTotalScore).ToArray(); winningUserId = winningScores.Length == 1 ? (int)winningScores.Single().user_id : null; if (winningUserId != null) @@ -79,7 +79,7 @@ protected override async Task Begin() // Winner: losing player takes damage. SoloScore losingScore = scores.Single(u => u.user_id != winningUserId); - int attackDamage = winningTotalScore - (int)losingScore.total_score; + int attackDamage = winningTotalScore - (int)losingScore.TotalScoreWithoutMods; double attackMultiplier = State.DamageMultiplier + State.Users[winningUserId.Value].DamageMultiplier; State.Users[(int)losingScore.user_id].DamageInfo = Controller.Damage((int)losingScore.user_id, attackDamage, attackMultiplier, BaseDamage); @@ -92,7 +92,7 @@ await Controller.MatchmakingService.RecordBeatmapResult( Controller.Pool.id, Room.CurrentPlaylistItem.BeatmapID, Room.CurrentPlaylistItem.RequiredMods.ToArray(), - scores.Select(s => (int)s.total_score).ToArray(), + scores.Select(s => (int)s.TotalScoreWithoutMods).ToArray(), scores.Select(s => Controller.RatingByUser[(int)s.user_id]).ToArray()); } diff --git a/osu.Server.Spectator/Hubs/Multiplayer/MultiplayerHub.cs b/osu.Server.Spectator/Hubs/Multiplayer/MultiplayerHub.cs index 97fdc469..91743d05 100644 --- a/osu.Server.Spectator/Hubs/Multiplayer/MultiplayerHub.cs +++ b/osu.Server.Spectator/Hubs/Multiplayer/MultiplayerHub.cs @@ -288,6 +288,9 @@ public async Task ChangeUserMods(IEnumerable newMods) if (room == null) throw new InvalidOperationException("Attempted to operate on a null room"); + if (!room.MatchController.AllowUserModChanges) + throw new InvalidOperationException("User mod changes are not allowed."); + await room.ChangeUserMods(Context.GetUserId(), newMods); } } diff --git a/osu.Server.Spectator/Hubs/Multiplayer/MultiplayerHub_Matchmaking.cs b/osu.Server.Spectator/Hubs/Multiplayer/MultiplayerHub_Matchmaking.cs index d304e6d9..6e352fe9 100644 --- a/osu.Server.Spectator/Hubs/Multiplayer/MultiplayerHub_Matchmaking.cs +++ b/osu.Server.Spectator/Hubs/Multiplayer/MultiplayerHub_Matchmaking.cs @@ -34,6 +34,12 @@ public async Task MatchmakingJoinLobby() } } + // Provided for backwards compatibility. Can be removed 20261001. + public async Task MatchmakingJoinQueue(int poolId) + { + await MatchmakingJoinQueueWithParams(new MatchmakingJoinQueueRequest { PoolId = poolId }); + } + public async Task GetMatchmakingPoolsOfType(MatchmakingPoolType type) { using (var db = databaseFactory.GetInstance()) @@ -59,7 +65,7 @@ public async Task MatchmakingLeaveLobby() await matchmakingQueueService.RemoveFromLobbyAsync(userUsage.Item!); } - public async Task MatchmakingJoinQueue(int poolId) + public async Task MatchmakingJoinQueueWithParams(MatchmakingJoinQueueRequest request) { using (var db = databaseFactory.GetInstance()) { @@ -68,7 +74,9 @@ public async Task MatchmakingJoinQueue(int poolId) } using (var userUsage = await GetOrCreateLocalUserState()) - await matchmakingQueueService.AddToQueueAsync(userUsage.Item!, poolId); + await matchmakingQueueService.AddToQueueAsync(userUsage.Item!, request.PoolId, request.Mods); + + return new MatchmakingJoinQueueResponse(); } public async Task MatchmakingLeaveQueue() diff --git a/osu.Server.Spectator/Hubs/Multiplayer/Standard/StandardMatchController.cs b/osu.Server.Spectator/Hubs/Multiplayer/Standard/StandardMatchController.cs index a0a9051c..70df311e 100644 --- a/osu.Server.Spectator/Hubs/Multiplayer/Standard/StandardMatchController.cs +++ b/osu.Server.Spectator/Hubs/Multiplayer/Standard/StandardMatchController.cs @@ -25,6 +25,8 @@ public abstract class StandardMatchController : IMatchController public const int HOST_PLAYLIST_LIMIT = 50; public const int GUEST_PLAYLIST_LIMIT = 3; + public bool AllowUserModChanges => true; + public MultiplayerPlaylistItem CurrentItem => room.Playlist[currentPlaylistItemIndex]; protected StandardMatchRoomState State { get; }