Skip to content
Merged
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
21 changes: 21 additions & 0 deletions osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,27 @@ public void TestUserStyleSelectionExitedWhenBeatmapSetChanged()
AddUntilStep("style selection screen closed", () => this.ChildrenOfType<MultiplayerMatchFreestyleSelect>().SingleOrDefault()?.IsCurrentScreen() != true);
}

[Test]
public void TestMaxParticipantsAndSlots()
{
createRoom(() => new Room
{
Name = "Test Room",
Password = "password",
Playlist =
[
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}
],
MaxParticipants = 10
});

AddStep("turn max participants off", () => multiplayerClient.ChangeSettings(maxParticipants: null));
AddStep("turn max participants back on", () => multiplayerClient.ChangeSettings(maxParticipants: 8));
}

private void enterGameplay()
{
pressReadyButton();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading;
using System.Numerics;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
Expand Down Expand Up @@ -55,6 +56,68 @@ public void TestAddUser()
AddAssert("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 2);
}

[Test]
public void TestSlots()
{
setUpList();
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);

AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
{
Id = 3,
Username = "Second",
CoverUrl = TestResources.COVER_IMAGE_3,
}));

AddAssert("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 2);

AddStep("introduce slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
{
Slots = [null, 3, null, null, 1001, null, null]
}).WaitSafely());

AddStep("click first slot", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ParticipantPanel>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("slots changed", () => ((StandardMatchRoomState)MultiplayerClient.ClientRoom!.MatchState!).Slots,
() => Is.EquivalentTo(new int?[] { 1001, 3, null, null, null, null, null }));

AddStep("click second slot", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ParticipantPanel>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("slots not changed", () => ((StandardMatchRoomState)MultiplayerClient.ClientRoom!.MatchState!).Slots,
() => Is.EquivalentTo(new int?[] { 1001, 3, null, null, null, null, null }));

AddStep("click last slot", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ParticipantPanel>().Last());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("slots changed", () => ((StandardMatchRoomState)MultiplayerClient.ClientRoom!.MatchState!).Slots,
() => Is.EquivalentTo(new int?[] { null, 3, null, null, null, null, 1001 }));

AddStep("shuffle slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
{
Slots = [null, null, 1001, null, null, null, 3]
}).WaitSafely());
AddStep("remove slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
{
Slots = [null, 3, null, 1001]
}).WaitSafely());
AddStep("add slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
{
Slots = [null, null, 3, null, 1001, null]
}).WaitSafely());
AddStep("turn off slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
{
Slots = null
}).WaitSafely());
}

[Test]
public void TestAddReferee()
{
Expand Down Expand Up @@ -86,7 +149,7 @@ public void TestAddUnresolvedUser()

AddUntilStep("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 2);

AddStep("kick null user", () => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User == null)
AddStep("kick null user", () => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.User == null)
.ChildrenOfType<ParticipantPanel.KickButton>().Single().TriggerClick());

AddUntilStep("null user kicked", () => MultiplayerClient.ClientRoom.AsNonNull().Users.Count == 1);
Expand All @@ -111,7 +174,7 @@ public void TestRemoveUser()

AddStep("remove host", () => MultiplayerClient.RemoveUser(API.LocalUser.Value));

AddAssert("single panel is for second user", () => this.ChildrenOfType<ParticipantPanel>().Single().Current.Value.UserID == secondUser?.Id);
AddAssert("single panel is for second user", () => this.ChildrenOfType<ParticipantPanel>().Single().Current.Value.User?.UserID == secondUser?.Id);
}

[Test]
Expand Down Expand Up @@ -150,7 +213,7 @@ public void TestBeatmapDownloadingStates()

AddRepeatStep("increment progress", () =>
{
float progress = this.ChildrenOfType<ParticipantPanel>().Single().Current.Value.BeatmapAvailability.DownloadProgress ?? 0;
float progress = this.ChildrenOfType<ParticipantPanel>().Single().Current.Value.User?.BeatmapAvailability.DownloadProgress ?? 0;
MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress + RNG.NextSingle(0.1f)));
}, 25);

Expand Down Expand Up @@ -195,16 +258,16 @@ public void TestCrownChangesStateWhenHostTransferred()
}));

AddUntilStep("first user crown visible",
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.UserID == 1001).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.UserID == 1001).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
AddUntilStep("second user crown hidden",
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.UserID == 3).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.UserID == 3).ChildrenOfType<SpriteIcon>().First().Alpha == 0);

AddStep("make second user host", () => MultiplayerClient.TransferHost(3));

AddUntilStep("first user crown visible",
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.UserID == 1001).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.UserID == 1001).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
AddUntilStep("second user crown hidden",
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.UserID == 3).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.UserID == 3).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
}

[Test]
Expand All @@ -221,8 +284,8 @@ public void TestHostGetsPinnedToTop()
AddStep("make second user host", () => MultiplayerClient.TransferHost(3));
AddAssert("second user above first", () =>
{
var first = this.ChildrenOfType<ParticipantPanel>().Single(u => u.Current.Value.UserID == 1001);
var second = this.ChildrenOfType<ParticipantPanel>().Single(u => u.Current.Value.UserID == 3);
var first = this.ChildrenOfType<ParticipantPanel>().Single(u => u.Current.Value.User?.UserID == 1001);
var second = this.ChildrenOfType<ParticipantPanel>().Single(u => u.Current.Value.User?.UserID == 3);
return second.ScreenSpaceDrawQuad.TopLeft.Y < first.ScreenSpaceDrawQuad.TopLeft.Y;
});
}
Expand Down
17 changes: 17 additions & 0 deletions osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,23 @@ public void TestEnableAndDisablePassword()
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType<RoomPanel.CornerIcon>().First().Alpha));
}

[Test]
public void TestSetAndUnsetMaxParticipants()
{
RoomPanel panel = null!;
Room room = null!;

AddStep("create room", () => Child = panel = createLoungeRoom(room = new Room
{
Name = "A room",
Type = MatchType.HeadToHead,
}));

AddUntilStep("wait for panel load", () => panel.ChildrenOfType<DrawableRoomParticipantsList>().Any());
AddStep("set max participants", () => room.MaxParticipants = 5);
AddStep("unset max participants", () => room.MaxParticipants = null);
}

[Test]
public void TestMultiplayerRooms()
{
Expand Down
20 changes: 20 additions & 0 deletions osu.Game/Online/Multiplayer/ChangeSlotRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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 MessagePack;

namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// User requests to change their slot in the room.
/// </summary>
[MessagePackObject]
public class ChangeSlotRequest : MatchUserRequest
{
/// <summary>
/// The zero-based ID of the desired slot.
/// </summary>
[Key(0)]
public byte SlotID { get; set; }
}
}
1 change: 1 addition & 0 deletions osu.Game/Online/Multiplayer/MatchRoomState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ namespace osu.Game.Online.Multiplayer
[Union(0, typeof(TeamVersusRoomState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
[Union(1, typeof(MatchmakingRoomState))]
[Union(2, typeof(RankedPlayRoomState))]
[Union(3, typeof(StandardMatchRoomState))]
public abstract class MatchRoomState
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,20 @@
namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
{
[MessagePackObject]
public class TeamVersusRoomState : MatchRoomState
public class TeamVersusRoomState : StandardMatchRoomState
{
[Key(0)]
public List<MultiplayerTeam> Teams { get; set; } = new List<MultiplayerTeam>();

[Key(1)]
public bool Locked { get; set; }

public static TeamVersusRoomState CreateDefault() =>
public static TeamVersusRoomState CreateDefault(byte? maxParticipants = null) =>
new TeamVersusRoomState
{
Teams =
{
new MultiplayerTeam { ID = 0, Name = "Team Red" },
new MultiplayerTeam { ID = 1, Name = "Team Blue" },
}
},
Slots = maxParticipants == null ? null : new int?[maxParticipants.Value]
};
}
}
1 change: 1 addition & 0 deletions osu.Game/Online/Multiplayer/MatchUserRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ namespace osu.Game.Online.Multiplayer
[Union(4, typeof(RankedPlayCardHandReplayRequest))]
[Union(5, typeof(SetLockStateRequest))]
[Union(6, typeof(RollRequest))]
[Union(7, typeof(ChangeSlotRequest))]
public abstract class MatchUserRequest
{
}
Expand Down
6 changes: 4 additions & 2 deletions osu.Game/Online/Multiplayer/MultiplayerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -403,8 +403,9 @@ await runOnUpdateThreadAsync(() =>
/// <param name="queueMode">The new queue mode, if any.</param>
/// <param name="autoStartDuration">The new auto-start countdown duration, if any.</param>
/// <param name="autoSkip">The new auto-skip setting.</param>
/// <param name="maxParticipants">The new participant count limit, if any.</param>
public Task ChangeSettings(Optional<string> name = default, Optional<string> password = default, Optional<MatchType> matchType = default, Optional<QueueMode> queueMode = default,
Optional<TimeSpan> autoStartDuration = default, Optional<bool> autoSkip = default)
Optional<TimeSpan> autoStartDuration = default, Optional<bool> autoSkip = default, Optional<byte?> maxParticipants = default)
{
if (Room == null)
throw new InvalidOperationException("Must be joined to a match to change settings.");
Expand All @@ -416,7 +417,8 @@ public Task ChangeSettings(Optional<string> name = default, Optional<string> pas
MatchType = matchType.GetOr(Room.Settings.MatchType),
QueueMode = queueMode.GetOr(Room.Settings.QueueMode),
AutoStartDuration = autoStartDuration.GetOr(Room.Settings.AutoStartDuration),
AutoSkip = autoSkip.GetOr(Room.Settings.AutoSkip)
AutoSkip = autoSkip.GetOr(Room.Settings.AutoSkip),
MaxParticipants = maxParticipants.GetOr(Room.Settings.MaxParticipants),
});
}

Expand Down
10 changes: 8 additions & 2 deletions osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public class MultiplayerRoomSettings : IEquatable<MultiplayerRoomSettings>
[Key(6)]
public bool AutoSkip { get; set; }

[Key(7)]
public byte? MaxParticipants { get; set; }

[IgnoreMember]
public bool AutoStartEnabled => AutoStartDuration != TimeSpan.Zero;

Expand All @@ -47,6 +50,7 @@ public MultiplayerRoomSettings(Room room)
QueueMode = room.QueueMode;
AutoStartDuration = room.AutoStartDuration;
AutoSkip = room.AutoSkip;
MaxParticipants = room.MaxParticipants;
}

public bool Equals(MultiplayerRoomSettings? other)
Expand All @@ -60,7 +64,8 @@ public bool Equals(MultiplayerRoomSettings? other)
&& MatchType == other.MatchType
&& QueueMode == other.QueueMode
&& AutoStartDuration == other.AutoStartDuration
&& AutoSkip == other.AutoSkip;
&& AutoSkip == other.AutoSkip
&& MaxParticipants == other.MaxParticipants;
}

public override string ToString() => $"Name:{Name}"
Expand All @@ -69,6 +74,7 @@ public override string ToString() => $"Name:{Name}"
+ $" Item:{PlaylistItemId}"
+ $" Queue:{QueueMode}"
+ $" Start:{AutoStartDuration}"
+ $" AutoSkip:{AutoSkip}";
+ $" AutoSkip:{AutoSkip}"
+ $" MaxParticipants:{MaxParticipants?.ToString() ?? "no limit"}";
}
}
5 changes: 2 additions & 3 deletions osu.Game/Online/Multiplayer/SetLockStateRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ public class SetLockStateRequest : MatchUserRequest
{
/// <summary>
/// <para>
/// If <see langword="true"/>, <see cref="MultiplayerRoomUserRole.Player"/>s will not be able to change teams by themselves in the room,
/// If <see langword="true"/>, <see cref="MultiplayerRoomUserRole.Player"/>s will not be able to change teams and slots by themselves in the room,
/// only <see cref="MultiplayerRoomUserRole.Referee"/>s will be able to change teams for the <see cref="MultiplayerRoomUserRole.Player"/>s.
/// </para>
/// <para>
/// If <see langword="false"/>, any user can change their team in the room.
/// If <see langword="false"/>, any user can change their team and slot in the room.
/// </para>
/// </summary>
// TODO: mention slots as well when slots are reimplemented
[Key(0)]
public bool Locked { get; set; }
}
Expand Down
39 changes: 39 additions & 0 deletions osu.Game/Online/Multiplayer/StandardMatchRoomState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 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 MessagePack;

namespace osu.Game.Online.Multiplayer
{
[MessagePackObject]
public class StandardMatchRoomState : MatchRoomState
{
/// <summary>
/// Whether the room is currently locked.
/// When locked, changes to slots (and teams, in team versus) cannot be performed by anyone but room referees.
/// </summary>
[Key(1)]
public bool Locked { get; set; }

/// <summary>
/// The state of slots in the room.
/// Linked to <see cref="MultiplayerRoomSettings.MaxParticipants"/>.
/// <list type="bullet">
/// <item>When <see cref="MultiplayerRoomSettings.MaxParticipants"/> is <see langword="null"/>, this property is also <see langword="null"/>.</item>
/// <item>
/// When <see cref="MultiplayerRoomSettings.MaxParticipants"/> is not <see langword="null"/>, this property is an array of that length.
/// The items of that array represent either an empty slot (represented by <see langword="null"/>),
/// or an user occupying that slot (represented by the ID of the relevant user).
/// </item>
/// </list>
/// </summary>
[Key(2)]
public int?[]? Slots { get; set; }

public static StandardMatchRoomState Create(byte? maxParticipants = null) =>
new StandardMatchRoomState
{
Slots = maxParticipants == null ? null : new int?[maxParticipants.Value]
};
}
}
3 changes: 2 additions & 1 deletion osu.Game/Online/Rooms/Room.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public DateTimeOffset? EndDate
/// <summary>
/// The maximum number of users allowed in the room.
/// </summary>
public int? MaxParticipants
public byte? MaxParticipants
{
get;
set => SetField(ref field, value);
Expand Down Expand Up @@ -318,6 +318,7 @@ public Room(MultiplayerRoom room)
QueueMode = room.Settings.QueueMode;
AutoStartDuration = room.Settings.AutoStartDuration;
AutoSkip = room.Settings.AutoSkip;
MaxParticipants = room.Settings.MaxParticipants;
Host = room.Host != null ? new APIUser { Id = room.Host.UserID } : null;
Playlist = room.Playlist.Select(p => new PlaylistItem(p)).ToArray();
}
Expand Down
2 changes: 2 additions & 0 deletions osu.Game/Online/SignalRWorkaroundTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ internal static class SignalRWorkaroundTypes
internal static readonly IReadOnlyList<(Type derivedType, Type baseType)> BASE_TYPE_MAPPING = new[]
{
// multiplayer
(typeof(ChangeSlotRequest), typeof(MatchUserRequest)),
(typeof(ChangeTeamRequest), typeof(MatchUserRequest)),
(typeof(StartMatchCountdownRequest), typeof(MatchUserRequest)),
(typeof(StopCountdownRequest), typeof(MatchUserRequest)),
Expand All @@ -32,6 +33,7 @@ internal static class SignalRWorkaroundTypes
(typeof(CountdownStartedEvent), typeof(MatchServerEvent)),
(typeof(CountdownStoppedEvent), typeof(MatchServerEvent)),
(typeof(RollEvent), typeof(MatchServerEvent)),
(typeof(StandardMatchRoomState), typeof(MatchRoomState)),
(typeof(TeamVersusRoomState), typeof(MatchRoomState)),
(typeof(TeamVersusUserState), typeof(MatchUserState)),
(typeof(MatchStartCountdown), typeof(MultiplayerCountdown)),
Expand Down
Loading
Loading