Skip to content

Commit 15ed029

Browse files
authored
Merge pull request #31260 from smoogipoo/multiplayer-free-style
Add support for "freestyle" in multiplayer
2 parents 87ff877 + a93dabd commit 15ed029

36 files changed

+964
-159
lines changed

osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,15 @@ private void load(GameHost host, AudioManager audio)
6060

6161
private void setUp()
6262
{
63-
AddStep("reset", () =>
63+
AddStep("create song select", () =>
6464
{
6565
Ruleset.Value = new OsuRuleset().RulesetInfo;
6666
Beatmap.SetDefault();
6767
SelectedMods.SetDefault();
68+
69+
LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!));
6870
});
6971

70-
AddStep("create song select", () => LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!)));
7172
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
7273
}
7374

osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
using osu.Game.Screens.OnlinePlay.Match;
3131
using osu.Game.Screens.OnlinePlay.Multiplayer;
3232
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
33+
using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
3334
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
3435
using osu.Game.Tests.Beatmaps;
3536
using osu.Game.Tests.Resources;
@@ -271,7 +272,10 @@ public void TestNextPlaylistItemSelectedAfterCompletion()
271272

272273
AddUntilStep("last playlist item selected", () =>
273274
{
274-
var lastItem = this.ChildrenOfType<DrawableRoomPlaylistItem>().Single(p => p.Item.ID == MultiplayerClient.ServerAPIRoom?.Playlist.Last().ID);
275+
var lastItem = this.ChildrenOfType<MultiplayerQueueList>()
276+
.Single()
277+
.ChildrenOfType<DrawableRoomPlaylistItem>()
278+
.Single(p => p.Item.ID == MultiplayerClient.ServerAPIRoom?.Playlist.Last().ID);
275279
return lastItem.IsSelectedItem;
276280
});
277281
}

osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs

+27
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,33 @@ public void TestUserWithMods()
308308
AddStep("set state: locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable()));
309309
}
310310

311+
[Test]
312+
public void TestUserWithStyle()
313+
{
314+
AddStep("add users", () =>
315+
{
316+
MultiplayerClient.AddUser(new APIUser
317+
{
318+
Id = 0,
319+
Username = "User 0",
320+
RulesetsStatistics = new Dictionary<string, UserStatistics>
321+
{
322+
{
323+
Ruleset.Value.ShortName,
324+
new UserStatistics { GlobalRank = RNG.Next(1, 100000), }
325+
}
326+
},
327+
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
328+
});
329+
330+
MultiplayerClient.ChangeUserStyle(0, 259, 2);
331+
});
332+
333+
AddStep("set beatmap locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable()));
334+
AddStep("change user style to beatmap: 258, ruleset: 1", () => MultiplayerClient.ChangeUserStyle(0, 258, 1));
335+
AddStep("change user style to beatmap: null, ruleset: null", () => MultiplayerClient.ChangeUserStyle(0, null, null));
336+
}
337+
311338
[Test]
312339
public void TestModOverlap()
313340
{

osu.Game/Localisation/MultiplayerMatchStrings.cs

+15
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,21 @@ public static class MultiplayerMatchStrings
2424
/// </summary>
2525
public static LocalisableString StartMatchWithCountdown(string humanReadableTime) => new TranslatableString(getKey(@"start_match_width_countdown"), @"Start match in {0}", humanReadableTime);
2626

27+
/// <summary>
28+
/// "Choose the mods which all players should play with."
29+
/// </summary>
30+
public static LocalisableString RequiredModsButtonTooltip => new TranslatableString(getKey(@"required_mods_button_tooltip"), @"Choose the mods which all players should play with.");
31+
32+
/// <summary>
33+
/// "Each player can choose their preferred mods from a selected list."
34+
/// </summary>
35+
public static LocalisableString FreeModsButtonTooltip => new TranslatableString(getKey(@"free_mods_button_tooltip"), @"Each player can choose their preferred mods from a selected list.");
36+
37+
/// <summary>
38+
/// "Each player can choose their preferred difficulty, ruleset and mods."
39+
/// </summary>
40+
public static LocalisableString FreestyleButtonTooltip => new TranslatableString(getKey(@"freestyle_button_tooltip"), @"Each player can choose their preferred difficulty, ruleset and mods.");
41+
2742
private static string getKey(string key) => $@"{prefix}:{key}";
2843
}
2944
}

osu.Game/Online/Multiplayer/IMultiplayerClient.cs

+8
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ public interface IMultiplayerClient : IStatefulUserHubClient
9595
/// <param name="beatmapAvailability">The new beatmap availability state of the user.</param>
9696
Task UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability);
9797

98+
/// <summary>
99+
/// Signals that a user in this room changed their style.
100+
/// </summary>
101+
/// <param name="userId">The ID of the user whose style changed.</param>
102+
/// <param name="beatmapId">The user's beatmap.</param>
103+
/// <param name="rulesetId">The user's ruleset.</param>
104+
Task UserStyleChanged(int userId, int? beatmapId, int? rulesetId);
105+
98106
/// <summary>
99107
/// Signals that a user in this room changed their local mods.
100108
/// </summary>

osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs

+7
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ public interface IMultiplayerRoomServer
5757
/// <param name="newBeatmapAvailability">The proposed new beatmap availability state.</param>
5858
Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability);
5959

60+
/// <summary>
61+
/// Change the local user's style in the currently joined room.
62+
/// </summary>
63+
/// <param name="beatmapId">The beatmap.</param>
64+
/// <param name="rulesetId">The ruleset.</param>
65+
Task ChangeUserStyle(int? beatmapId, int? rulesetId);
66+
6067
/// <summary>
6168
/// Change the local user's mods in the currently joined room.
6269
/// </summary>

osu.Game/Online/Multiplayer/MultiplayerClient.cs

+21
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,8 @@ public async Task ToggleSpectate()
358358

359359
public abstract Task DisconnectInternal();
360360

361+
public abstract Task ChangeUserStyle(int? beatmapId, int? rulesetId);
362+
361363
/// <summary>
362364
/// Change the local user's mods in the currently joined room.
363365
/// </summary>
@@ -653,6 +655,25 @@ Task IMultiplayerClient.UserBeatmapAvailabilityChanged(int userId, BeatmapAvaila
653655
return Task.CompletedTask;
654656
}
655657

658+
public Task UserStyleChanged(int userId, int? beatmapId, int? rulesetId)
659+
{
660+
Scheduler.Add(() =>
661+
{
662+
var user = Room?.Users.SingleOrDefault(u => u.UserID == userId);
663+
664+
// errors here are not critical - user style is mostly for display.
665+
if (user == null)
666+
return;
667+
668+
user.BeatmapId = beatmapId;
669+
user.RulesetId = rulesetId;
670+
671+
RoomUpdated?.Invoke();
672+
}, false);
673+
674+
return Task.CompletedTask;
675+
}
676+
656677
public Task UserModsChanged(int userId, IEnumerable<APIMod> mods)
657678
{
658679
Scheduler.Add(() =>

osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs

+15-3
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ public class MultiplayerRoomUser : IEquatable<MultiplayerRoomUser>
2222
[Key(1)]
2323
public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle;
2424

25-
[Key(4)]
26-
public MatchUserState? MatchState { get; set; }
27-
2825
/// <summary>
2926
/// The availability state of the current beatmap.
3027
/// </summary>
@@ -37,6 +34,21 @@ public class MultiplayerRoomUser : IEquatable<MultiplayerRoomUser>
3734
[Key(3)]
3835
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
3936

37+
[Key(4)]
38+
public MatchUserState? MatchState { get; set; }
39+
40+
/// <summary>
41+
/// If not-null, a local override for this user's ruleset selection.
42+
/// </summary>
43+
[Key(5)]
44+
public int? RulesetId;
45+
46+
/// <summary>
47+
/// If not-null, a local override for this user's beatmap selection.
48+
/// </summary>
49+
[Key(6)]
50+
public int? BeatmapId;
51+
4052
[IgnoreMember]
4153
public APIUser? User { get; set; }
4254

osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs

+11
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ private void load(IAPIProvider api)
6060
connection.On(nameof(IMultiplayerClient.GameplayStarted), ((IMultiplayerClient)this).GameplayStarted);
6161
connection.On<GameplayAbortReason>(nameof(IMultiplayerClient.GameplayAborted), ((IMultiplayerClient)this).GameplayAborted);
6262
connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
63+
connection.On<int, int?, int?>(nameof(IMultiplayerClient.UserStyleChanged), ((IMultiplayerClient)this).UserStyleChanged);
6364
connection.On<int, IEnumerable<APIMod>>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
6465
connection.On<int, BeatmapAvailability>(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);
6566
connection.On<MatchRoomState>(nameof(IMultiplayerClient.MatchRoomStateChanged), ((IMultiplayerClient)this).MatchRoomStateChanged);
@@ -186,6 +187,16 @@ public override Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAva
186187
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability);
187188
}
188189

190+
public override Task ChangeUserStyle(int? beatmapId, int? rulesetId)
191+
{
192+
if (!IsConnected.Value)
193+
return Task.CompletedTask;
194+
195+
Debug.Assert(connection != null);
196+
197+
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserStyle), beatmapId, rulesetId);
198+
}
199+
189200
public override Task ChangeUserMods(IEnumerable<APIMod> newMods)
190201
{
191202
if (!IsConnected.Value)

osu.Game/Online/Rooms/CreateRoomScoreRequest.cs

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ protected override WebRequest CreateWebRequest()
3131
var req = base.CreateWebRequest();
3232
req.Method = HttpMethod.Post;
3333
req.AddParameter("version_hash", versionHash);
34+
req.AddParameter("beatmap_id", beatmapInfo.OnlineID.ToString(CultureInfo.InvariantCulture));
3435
req.AddParameter("beatmap_hash", beatmapInfo.MD5Hash);
3536
req.AddParameter("ruleset_id", rulesetId.ToString(CultureInfo.InvariantCulture));
3637
return req;

osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs

+6
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ public class MultiplayerPlaylistItem
5656
[Key(10)]
5757
public double StarRating { get; set; }
5858

59+
/// <summary>
60+
/// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset.
61+
/// </summary>
62+
[Key(11)]
63+
public bool Freestyle { get; set; }
64+
5965
[SerializationConstructor]
6066
public MultiplayerPlaylistItem()
6167
{

osu.Game/Online/Rooms/MultiplayerScore.cs

+7-4
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,14 @@ public class MultiplayerScore
7777
[CanBeNull]
7878
public MultiplayerScoresAround ScoresAround { get; set; }
7979

80-
public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap)
80+
[JsonProperty("ruleset_id")]
81+
public int RulesetId { get; set; }
82+
83+
public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, [NotNull] BeatmapInfo beatmap)
8184
{
82-
var ruleset = rulesets.GetRuleset(playlistItem.RulesetID);
85+
var ruleset = rulesets.GetRuleset(RulesetId);
8386
if (ruleset == null)
84-
throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {playlistItem.RulesetID}");
87+
throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {RulesetId}");
8588

8689
var rulesetInstance = ruleset.CreateInstance();
8790

@@ -91,7 +94,7 @@ public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore ruleset
9194
TotalScore = TotalScore,
9295
MaxCombo = MaxCombo,
9396
BeatmapInfo = beatmap,
94-
Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {playlistItem.RulesetID} not found locally"),
97+
Ruleset = ruleset,
9598
Passed = Passed,
9699
Statistics = Statistics,
97100
MaximumStatistics = MaximumStatistics,

osu.Game/Online/Rooms/PlaylistItem.cs

+12-3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ private int onlineBeatmapId
6767
set => Beatmap = new APIBeatmap { OnlineID = value };
6868
}
6969

70+
/// <summary>
71+
/// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset.
72+
/// </summary>
73+
[JsonProperty("freestyle")]
74+
public bool Freestyle { get; set; }
75+
7076
/// <summary>
7177
/// A beatmap representing this playlist item.
7278
/// In many cases, this will *not* contain any usable information apart from OnlineID.
@@ -101,6 +107,7 @@ public PlaylistItem(MultiplayerPlaylistItem item)
101107
PlayedAt = item.PlayedAt;
102108
RequiredMods = item.RequiredMods.ToArray();
103109
AllowedMods = item.AllowedMods.ToArray();
110+
Freestyle = item.Freestyle;
104111
}
105112

106113
public void MarkInvalid() => valid.Value = false;
@@ -120,18 +127,19 @@ public PlaylistItem(MultiplayerPlaylistItem item)
120127

121128
#endregion
122129

123-
public PlaylistItem With(Optional<long> id = default, Optional<IBeatmapInfo> beatmap = default, Optional<ushort?> playlistOrder = default)
130+
public PlaylistItem With(Optional<long> id = default, Optional<IBeatmapInfo> beatmap = default, Optional<ushort?> playlistOrder = default, Optional<int> ruleset = default)
124131
{
125132
return new PlaylistItem(beatmap.GetOr(Beatmap))
126133
{
127134
ID = id.GetOr(ID),
128135
OwnerID = OwnerID,
129-
RulesetID = RulesetID,
136+
RulesetID = ruleset.GetOr(RulesetID),
130137
Expired = Expired,
131138
PlaylistOrder = playlistOrder.GetOr(PlaylistOrder),
132139
PlayedAt = PlayedAt,
133140
AllowedMods = AllowedMods,
134141
RequiredMods = RequiredMods,
142+
Freestyle = Freestyle,
135143
valid = { Value = Valid.Value },
136144
};
137145
}
@@ -143,6 +151,7 @@ public bool Equals(PlaylistItem? other)
143151
&& Expired == other.Expired
144152
&& PlaylistOrder == other.PlaylistOrder
145153
&& AllowedMods.SequenceEqual(other.AllowedMods)
146-
&& RequiredMods.SequenceEqual(other.RequiredMods);
154+
&& RequiredMods.SequenceEqual(other.RequiredMods)
155+
&& Freestyle == other.Freestyle;
147156
}
148157
}

osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ private void load(AudioManager audio)
161161
{
162162
new Drawable[]
163163
{
164-
new DrawableRoomPlaylistItem(playlistItem)
164+
new DrawableRoomPlaylistItem(playlistItem, true)
165165
{
166166
RelativeSizeAxes = Axes.X,
167167
AllowReordering = false,

osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,10 @@ public void RefetchScores()
142142

143143
request.Success += req => Schedule(() =>
144144
{
145-
var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo)).ToArray();
145+
var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, beatmap.Value.BeatmapInfo)).ToArray();
146146

147147
userBestScore.Value = req.UserScore;
148-
var userBest = userBestScore.Value?.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo);
148+
var userBest = userBestScore.Value?.CreateScoreInfo(scoreManager, rulesets, beatmap.Value.BeatmapInfo);
149149

150150
cancellationTokenSource?.Cancel();
151151
cancellationTokenSource = null;

osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public partial class DrawableRoomPlaylistItem : OsuRearrangeableListItem<Playlis
7474

7575
public bool IsSelectedItem => SelectedItem.Value?.ID == Item.ID;
7676

77-
private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both };
77+
private readonly DelayedLoadWrapper onScreenLoader;
7878
private readonly IBindable<bool> valid = new Bindable<bool>();
7979

8080
private IBeatmapInfo? beatmap;
@@ -120,9 +120,11 @@ public partial class DrawableRoomPlaylistItem : OsuRearrangeableListItem<Playlis
120120
[Resolved(CanBeNull = true)]
121121
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
122122

123-
public DrawableRoomPlaylistItem(PlaylistItem item)
123+
public DrawableRoomPlaylistItem(PlaylistItem item, bool loadImmediately = false)
124124
: base(item)
125125
{
126+
onScreenLoader = new DelayedLoadWrapper(Empty, timeBeforeLoad: loadImmediately ? 0 : 500) { RelativeSizeAxes = Axes.Both };
127+
126128
Item = item;
127129

128130
valid.BindTo(item.Valid);

0 commit comments

Comments
 (0)