Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion SampleMultiplayerClient/MultiplayerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,8 @@ public Task<MatchmakingJoinLobbyResponse> MatchmakingJoinLobbyWithParams(Matchma

public Task MatchmakingLeaveLobby() => connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingLeaveLobby));

public Task MatchmakingJoinQueue(int poolId) => connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingJoinQueue), poolId);
public Task<MatchmakingJoinQueueResponse> MatchmakingJoinQueueWithParams(MatchmakingJoinQueueRequest request)
=> connection.InvokeAsync<MatchmakingJoinQueueResponse>(nameof(IMatchmakingServer.MatchmakingJoinQueueWithParams), request);

public Task MatchmakingLeaveQueue() => connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingLeaveQueue));

Expand Down
2 changes: 1 addition & 1 deletion SampleMultiplayerClient/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();

Expand All @@ -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<MatchmakingRoomInvitationParams>()), Times.Never);
User2Receiver.Verify(u => u.MatchmakingRoomInvitedWithParams(It.IsAny<MatchmakingRoomInvitationParams>()), 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<MatchmakingRoomInvitationParams>()), Times.Once);
Expand All @@ -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<MatchmakingRoomInvitationParams>()), Times.Once);
Expand All @@ -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<MatchmakingRoomInvitationParams>()), Times.Once);
Expand All @@ -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<MatchmakingRoomInvitationParams>()), Times.Once);
Expand All @@ -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<MatchmakingRoomInvitationParams>()), Times.Once);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// 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 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<long>(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<int>(), It.IsAny<uint>()))
.Returns<int, uint>((userId, poolId) => Task.FromResult<matchmaking_user_stats?>(new matchmaking_user_stats
{
user_id = (uint)userId,
pool_id = poolId
}));

Database.Setup(db => db.GetAllScoresForPlaylistItem(It.IsAny<long>()))
.Returns<long>(_ => Task.FromResult<IEnumerable<SoloScore>>(
[
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<IDatabaseFactory>().Object),
new Mock<IMatchmakingQueueBackgroundService>().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<IDatabaseFactory>().Object),
new Mock<IMatchmakingQueueBackgroundService>().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<InvalidOperationException>(() => Hub.ChangeUserMods([]));
}
}
}
26 changes: 26 additions & 0 deletions osu.Server.Spectator.Tests/RankedPlay/Stages/ResultsStageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<long>()))
.Returns<long>(_ => Task.FromResult<IEnumerable<SoloScore>>(
[
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()
{
Expand Down
3 changes: 3 additions & 0 deletions osu.Server.Spectator/Database/Models/SoloScore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -84,5 +85,7 @@ public string data
PP = pp,
HasReplay = has_replay
};

public long TotalScoreWithoutMods => ScoreData.TotalScoreWithoutMods ?? total_score;
}
}
3 changes: 3 additions & 0 deletions osu.Server.Spectator/Database/Models/SoloScoreData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@ public class SoloScoreData

[JsonProperty("maximum_statistics")]
public Dictionary<HitResult, int> MaximumStatistics { get; set; } = new Dictionary<HitResult, int>();

[JsonProperty("total_score_without_mods")]
public long? TotalScoreWithoutMods { get; set; }
}
}
5 changes: 5 additions & 0 deletions osu.Server.Spectator/Hubs/Multiplayer/IMatchController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ namespace osu.Server.Spectator.Hubs.Multiplayer
{
public interface IMatchController
{
/// <summary>
/// Whether users should be able to change their own mods within the match.
/// </summary>
bool AllowUserModChanges { get; }

MultiplayerPlaylistItem CurrentItem { get; }

Task Initialise();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ public class MatchmakingMatchController : IMatchController, IMatchmakingMatchCon
/// </summary>
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public interface IMatchmakingQueueBackgroundService : IHostedService
/// <summary>
/// Adds a user to the matchmaking queue.
/// </summary>
Task AddToQueueAsync(MultiplayerClientState state, int poolId);
Task AddToQueueAsync(MultiplayerClientState state, int poolId, APIMod[] mods);

/// <summary>
/// Removes a user from the matchmaking queue.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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)));
}
}

Expand Down Expand Up @@ -211,7 +215,7 @@ public async Task<MatchmakingIssueDuelResponse> 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
Expand Down Expand Up @@ -253,7 +257,7 @@ public async Task<MatchmakingAcceptDuelResponse> 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));

Expand Down Expand Up @@ -516,7 +520,7 @@ private async Task processBundle(MatchmakingQueueUpdateBundle bundle)
}
}

private async Task<MatchmakingQueueUser> createUserAsync(MultiplayerClientState state, matchmaking_pool pool)
private async Task<MatchmakingQueueUser> createUserAsync(MultiplayerClientState state, matchmaking_pool pool, APIMod[] mods)
{
using (var db = databaseFactory.GetInstance())
{
Expand All @@ -540,7 +544,7 @@ private async Task<MatchmakingQueueUser> createUserAsync(MultiplayerClientState
});
}

return new MatchmakingQueueUser(state.ConnectionId)
return new MatchmakingQueueUser(state.ConnectionId, mods)
{
UserId = state.UserId,
Rating = stats.EloData.Rating,
Expand Down
Loading
Loading