mirror of
https://github.com/ppy/osu.git
synced 2026-05-20 16:00:46 +08:00
Various tournament-minded improvements for multiplayer (#37000)
https://github.com/user-attachments/assets/042311f1-b8c3-479b-a173-13b93fe2c5cc - `/roll` command is now supported in multiplayer chat for all players (don't need to be a referee). - Referees are shown in the room with a special status. - Tournament mode rooms can be locked, which prevents users from changing team (and slot, whenever those get brought back). When the room is locked, the user team indicator shows a padlock icon on top to indicate the lock state. --- - Related: https://github.com/ppy/osu-server-spectator/issues/406 Grab-bag because I really don't think splitting this into 3 PRs is very helpful. I was going to add an animation for rolling but I had a go on Friday and my attempt got more or less the same reception as a wet fart so I'm not trying again. Someone else can if so inclined. I have completely lost trust in my design senses. Contrary to stable the roll results are completely ephemeral and go away when the room is re-entered. This could be both considered good and bad. Not for me to say.
This commit is contained in:
committed by
GitHub
Unverified
parent
b98cf42bd3
commit
43c53b5a8d
@@ -17,6 +17,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
@@ -57,6 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(Realm);
|
||||
Dependencies.CacheAs<BeatmapStore>(new RealmDetachedBeatmapStore());
|
||||
Dependencies.Cache(new ChannelManager(API));
|
||||
|
||||
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
|
||||
@@ -439,6 +441,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("countdown started", () => MultiplayerClient.ServerRoom!.ActiveCountdowns.Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRoll()
|
||||
{
|
||||
AddStep("set playlist", () =>
|
||||
{
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
|
||||
}
|
||||
];
|
||||
});
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
AddStep("set channel", () => room.ChannelId = 1);
|
||||
|
||||
AddUntilStep("wait for room join", () => RoomJoined);
|
||||
|
||||
AddStep("roll", () => MultiplayerClient.SendMatchRequest(new RollRequest()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSettingsRemainsOpenOnRoomUpdate()
|
||||
{
|
||||
|
||||
@@ -56,6 +56,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddReferee()
|
||||
{
|
||||
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
|
||||
|
||||
AddStep("add user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(3)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 3,
|
||||
Username = "Second",
|
||||
CoverUrl = TestResources.COVER_IMAGE_3,
|
||||
},
|
||||
Role = MultiplayerRoomUserRole.Referee
|
||||
}));
|
||||
|
||||
AddAssert("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddUnresolvedUser()
|
||||
{
|
||||
|
||||
@@ -116,6 +116,55 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("user still on team 0", () => (multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTeamChangesLocked()
|
||||
{
|
||||
createRoom(() => new Room
|
||||
{
|
||||
Name = "Test Room",
|
||||
Type = MatchType.TeamVersus,
|
||||
Playlist =
|
||||
[
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
AddUntilStep("user on team 0", () => (multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
|
||||
AddStep("add another user", () => multiplayerClient.AddUser(new APIUser { Username = "otheruser", Id = 44 }));
|
||||
|
||||
AddStep("lock room", () =>
|
||||
{
|
||||
var roomState = TeamVersusRoomState.CreateDefault();
|
||||
roomState.Locked = true;
|
||||
multiplayerClient.ChangeMatchRoomState(roomState).WaitSafely();
|
||||
});
|
||||
AddStep("press own button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType<TeamDisplay>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("user on team 0", () => (multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
|
||||
|
||||
AddStep("unlock room", () =>
|
||||
{
|
||||
var roomState = TeamVersusRoomState.CreateDefault();
|
||||
roomState.Locked = false;
|
||||
multiplayerClient.ChangeMatchRoomState(roomState).WaitSafely();
|
||||
});
|
||||
AddStep("press own button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType<TeamDisplay>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("user on team 1", () => (multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1);
|
||||
|
||||
AddStep("press own button again", () => InputManager.Click(MouseButton.Left));
|
||||
AddUntilStep("user on team 0", () => (multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSettingsUpdatedWhenChangingMatchType()
|
||||
{
|
||||
|
||||
@@ -7,6 +7,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
@@ -18,6 +20,7 @@ using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Overlays.Chat.Listing;
|
||||
|
||||
namespace osu.Game.Online.Chat
|
||||
@@ -70,6 +73,10 @@ namespace osu.Game.Online.Chat
|
||||
[Resolved]
|
||||
private UserLookupCache users { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private MultiplayerClient multiplayerClient { get; set; }
|
||||
|
||||
private readonly IBindable<APIUser> localUser = new Bindable<APIUser>();
|
||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||
private readonly IBindableList<APIRelation> localUserBlocks = new BindableList<APIRelation>();
|
||||
@@ -264,11 +271,11 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
switch (command.ToLowerInvariant())
|
||||
{
|
||||
case "np":
|
||||
case @"np":
|
||||
AddInternal(new NowPlayingCommand(target));
|
||||
break;
|
||||
|
||||
case "me":
|
||||
case @"me":
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
target.AddNewMessages(new ErrorMessage("Usage: /me [action]"));
|
||||
@@ -278,7 +285,7 @@ namespace osu.Game.Online.Chat
|
||||
PostMessage(content, true, target);
|
||||
break;
|
||||
|
||||
case "join":
|
||||
case @"join":
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
target.AddNewMessages(new ErrorMessage("Usage: /join [channel]"));
|
||||
@@ -296,9 +303,9 @@ namespace osu.Game.Online.Chat
|
||||
JoinChannel(channel);
|
||||
break;
|
||||
|
||||
case "chat":
|
||||
case "msg":
|
||||
case "query":
|
||||
case @"chat":
|
||||
case @"msg":
|
||||
case @"query":
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
target.AddNewMessages(new ErrorMessage($"Usage: /{command} [user]"));
|
||||
@@ -323,8 +330,36 @@ namespace osu.Game.Online.Chat
|
||||
api.Queue(request);
|
||||
break;
|
||||
|
||||
case "help":
|
||||
target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel], /chat [user], /np"));
|
||||
case @"roll":
|
||||
if (target.Type != ChannelType.Multiplayer || multiplayerClient?.Room?.ChannelID != target.Id)
|
||||
{
|
||||
target.AddNewMessages(new ErrorMessage("Cannot roll when not in a multiplayer room."));
|
||||
break;
|
||||
}
|
||||
|
||||
uint max = 100;
|
||||
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
{
|
||||
if (!uint.TryParse(content, out max) || max < 2 || max > 100)
|
||||
{
|
||||
target.AddNewMessages(new ErrorMessage("Usage: /roll [2-100]"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var rollRequest = new RollRequest { Max = max };
|
||||
multiplayerClient.SendMatchRequest(rollRequest).FireAndForget(onError: ex =>
|
||||
{
|
||||
string message = ex is HubException
|
||||
? $"Failed to roll: {ex.Message}"
|
||||
: "Failed to roll.";
|
||||
target.AddNewMessages(new ErrorMessage(message));
|
||||
});
|
||||
break;
|
||||
|
||||
case @"help":
|
||||
target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel], /chat [user], /np, /roll [2-100] (multiplayer only)"));
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@@ -21,6 +22,7 @@ using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
@@ -145,6 +147,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
private FillFlowContainer userStyleSection = null!;
|
||||
private Container<DrawableRoomPlaylistItem> userStyleDisplayContainer = null!;
|
||||
|
||||
private MatchChatDisplay chat = null!;
|
||||
|
||||
private Sample? sampleStart;
|
||||
private IDisposable? userModsSelectOverlayRegistration;
|
||||
|
||||
@@ -370,7 +374,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new MatchChatDisplay(room)
|
||||
chat = new MatchChatDisplay(room)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
@@ -431,6 +435,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
client.UserStyleChanged += onUserStyleChanged;
|
||||
client.UserModsChanged += onUserModsChanged;
|
||||
client.LoadRequested += onLoadRequested;
|
||||
client.MatchEvent += onMatchEvent;
|
||||
|
||||
beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true);
|
||||
|
||||
@@ -594,6 +599,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
private void onMatchEvent(MatchServerEvent ev)
|
||||
{
|
||||
switch (ev)
|
||||
{
|
||||
case RollEvent rollEvent:
|
||||
var user = client.Room?.Users.SingleOrDefault(u => u.UserID == rollEvent.UserID)?.User ?? new APIUser { Username = "Unknown user" };
|
||||
string text = $"{user.Username} rolled {"point".ToQuantity(rollEvent.Result)} out of {rollEvent.Max}.";
|
||||
chat.Channel.Value?.AddNewMessages(new InfoMessage(text));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Responds to changes in the local user's beatmap availability to notify the server and prepare the gameplay session.
|
||||
/// </summary>
|
||||
|
||||
@@ -282,7 +282,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
Schedule(() => userModsDisplay.Current.Value = userRuleset == null ? Array.Empty<Mod>() : user.Mods.Select(m => m.ToMod(userRuleset)).ToList());
|
||||
}
|
||||
|
||||
userStateDisplay.UpdateStatus(user.State, user.BeatmapAvailability);
|
||||
userStateDisplay.UpdateStatus(user);
|
||||
|
||||
if (user.BeatmapAvailability.State == DownloadState.LocallyAvailable && user.State != MultiplayerUserState.Spectating)
|
||||
{
|
||||
|
||||
@@ -85,17 +85,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
|
||||
private OsuColour colours = null!;
|
||||
|
||||
public void UpdateStatus(MultiplayerUserState state, BeatmapAvailability availability)
|
||||
public void UpdateStatus(MultiplayerRoomUser user)
|
||||
{
|
||||
// the only case where the progress bar is used does its own local fade in.
|
||||
// starting by fading out is a sane default.
|
||||
progressBar.FadeOut(fade_time);
|
||||
this.FadeIn(fade_time);
|
||||
|
||||
switch (state)
|
||||
if (user.Role == MultiplayerRoomUserRole.Referee)
|
||||
{
|
||||
text.Text = "referee";
|
||||
icon.Icon = OsuIcon.EditorWhistle;
|
||||
icon.Colour = colours.BlueLight;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (user.State)
|
||||
{
|
||||
case MultiplayerUserState.Idle:
|
||||
showBeatmapAvailability(availability);
|
||||
showBeatmapAvailability(user.BeatmapAvailability);
|
||||
break;
|
||||
|
||||
case MultiplayerUserState.Ready:
|
||||
@@ -142,7 +150,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(state), state, null);
|
||||
throw new ArgumentOutOfRangeException(nameof(user.State), user.State, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@@ -39,6 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
|
||||
private OsuClickableContainer clickableContent = null!;
|
||||
private Drawable box = null!;
|
||||
private SpriteIcon lockIcon = null!;
|
||||
private Sample? sampleTeamSwap;
|
||||
|
||||
public TeamDisplay()
|
||||
@@ -57,20 +59,32 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
Alpha = 0,
|
||||
Scale = new Vector2(0, 1),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Child = box = new Container
|
||||
Children = new[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CornerRadius = 5,
|
||||
Masking = true,
|
||||
Scale = new Vector2(0, 1),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Child = new Box
|
||||
box = new Container
|
||||
{
|
||||
Colour = Color4.White,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CornerRadius = 5,
|
||||
Masking = true,
|
||||
Scale = new Vector2(0, 1),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Child = new Box
|
||||
{
|
||||
Colour = Color4.White,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
},
|
||||
lockIcon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(12),
|
||||
Icon = FontAwesome.Solid.Lock,
|
||||
Colour = Colour4.Black,
|
||||
Alpha = 0,
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -83,7 +97,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
base.LoadComplete();
|
||||
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
current.BindValueChanged(_ => updateUser(), true);
|
||||
current.BindValueChanged(_ => updateState(false), true);
|
||||
}
|
||||
|
||||
private void changeTeam()
|
||||
@@ -96,19 +110,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
|
||||
public int? DisplayedTeam { get; private set; }
|
||||
|
||||
private void updateUser()
|
||||
{
|
||||
var user = current.Value;
|
||||
|
||||
if (client.LocalUser?.Equals(user) == true)
|
||||
{
|
||||
clickableContent.Action = changeTeam;
|
||||
clickableContent.TooltipText = "Change team";
|
||||
}
|
||||
|
||||
updateState(false);
|
||||
}
|
||||
|
||||
private void onRoomUpdated() => Scheduler.AddOnce(() => updateState(true));
|
||||
|
||||
private void updateState(bool playSamples)
|
||||
@@ -118,8 +119,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
var user = current.Value;
|
||||
var userRoomState = client.Room?.Users.FirstOrDefault(u => u.Equals(user))?.MatchState;
|
||||
|
||||
bool roomLocked = (client.Room?.MatchState as TeamVersusRoomState)?.Locked == true;
|
||||
|
||||
if (client.LocalUser?.Equals(user) == true && !roomLocked)
|
||||
{
|
||||
clickableContent.Action = changeTeam;
|
||||
clickableContent.TooltipText = "Change team";
|
||||
}
|
||||
else
|
||||
{
|
||||
clickableContent.Action = null;
|
||||
clickableContent.TooltipText = default;
|
||||
}
|
||||
|
||||
const double duration = 400;
|
||||
|
||||
lockIcon.FadeTo(roomLocked ? 0.4f : 0, duration, Easing.OutQuint);
|
||||
|
||||
int? newTeam = (userRoomState as TeamVersusUserState)?.TeamID;
|
||||
|
||||
if (newTeam == DisplayedTeam)
|
||||
|
||||
@@ -10,6 +10,7 @@ using MessagePack;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API;
|
||||
@@ -433,6 +434,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
await StopCountdown(ServerRoom.ActiveCountdowns.First(c => c.ID == stopCountdown.ID)).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case RollRequest rollRequest:
|
||||
int max = (int)(rollRequest.Max ?? 100);
|
||||
await ((IMultiplayerClient)this).MatchEvent(new RollEvent
|
||||
{
|
||||
UserID = userId,
|
||||
Max = (uint)max,
|
||||
Result = (uint)RNG.Next(1, max + 1)
|
||||
}).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case MatchmakingAvatarActionRequest avatarAction:
|
||||
await ((IMultiplayerClient)this).MatchEvent(new MatchmakingAvatarActionEvent
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user