Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for "freestyle" in multiplayer #31260

Merged
merged 42 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a99a992
Adjust test to load song select during setup
smoogipoo Dec 9, 2024
9abb92a
Add BeatmapSetId to playlist items
smoogipoo Dec 10, 2024
0fb7523
Add "freeplay" button to multiplayer song select
smoogipoo Dec 10, 2024
5a2cae8
Fix free mod button overriding enabled state
smoogipoo Dec 10, 2024
159f602
Fix incorrect behaviour
smoogipoo Dec 18, 2024
638d959
Initial support for free style selection
smoogipoo Dec 23, 2024
7777c44
Only allow selecting beatmaps within 30s length
smoogipoo Dec 24, 2024
40486c4
Block beatmap presents in style select screen
smoogipoo Dec 24, 2024
971ccb6
Adjust namings
smoogipoo Dec 24, 2024
ac738f1
Add style selection to playlists screen
smoogipoo Dec 24, 2024
d8ff5bc
Fix freemods button opening overlay unexpectedly
smoogipoo Dec 24, 2024
c88e906
Add some comments
smoogipoo Dec 24, 2024
b4f35f3
Use online ruleset_id to build local score models
smoogipoo Dec 24, 2024
a2dc16f
Fix inspection
smoogipoo Dec 24, 2024
a407e3f
Fix co-variant array conversion
smoogipoo Dec 25, 2024
95fe8d6
Fix test
smoogipoo Dec 25, 2024
0093af8
Rewrite everything to better support spectator server messaging
smoogipoo Dec 25, 2024
c3aa9d6
Display user style in participant panel
smoogipoo Dec 25, 2024
e7c272b
Don't display on matching beatmap/ruleset
smoogipoo Dec 25, 2024
6579b05
Remove unused usings
smoogipoo Dec 25, 2024
9c05837
Change to using a 'FreeStyle' boolean
smoogipoo Jan 8, 2025
be33add
Fix possible null reference
smoogipoo Jan 8, 2025
46e9da7
Fix style display refreshing on all room updates
smoogipoo Jan 16, 2025
409ea53
Send `beatmap_id` when creating score
smoogipoo Jan 16, 2025
c68a0bc
Merge branch 'master' into multiplayer-free-style
peppy Jan 21, 2025
f881026
Add tooltips explaining multiplayer mod selection buttons
peppy Jan 21, 2025
459847c
Perform client side validation that the selected beatmap and ruleset …
peppy Jan 21, 2025
1bfb4d4
Merge branch 'master' into multiplayer-free-style
peppy Jan 24, 2025
17b1739
Combine countless update methods all called together into a single me…
peppy Jan 24, 2025
ca979d3
Adjust xmldocs
smoogipoo Jan 27, 2025
fc73037
Add pill displaying current freestyle status
smoogipoo Jan 27, 2025
d3f9804
Combine more methods to simplify flow
peppy Jan 29, 2025
05200e8
Add missing `partial`
peppy Jan 29, 2025
c70ff11
Remove new bindables from `RoomSubScreen`
peppy Jan 29, 2025
07bff22
Fix delay before difficulty panel displays fully
peppy Jan 29, 2025
e8d0d2a
Combine more methods to simplify flow futher
peppy Jan 29, 2025
bc930e8
Minimal clean-up to get things bearable
peppy Jan 29, 2025
444e097
Standardise naming to use "Freestyle" not "FreeStyle"
peppy Feb 3, 2025
37abb1a
Tidy up button construction code
peppy Feb 3, 2025
8bb7bea
Rename freestyle select screen classes for better discoverability
peppy Feb 3, 2025
6c60634
Remove `Scheduler.AddOnce` from `updateSpecifics`
peppy Feb 4, 2025
a93dabd
Merge branch 'master' into multiplayer-free-style
peppy Feb 4, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,15 @@ private void load(GameHost host, AudioManager audio)

private void setUp()
{
AddStep("reset", () =>
AddStep("create song select", () =>
{
Ruleset.Value = new OsuRuleset().RulesetInfo;
Beatmap.SetDefault();
SelectedMods.SetDefault();

LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!));
});

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
Expand Down Expand Up @@ -271,7 +272,10 @@ public void TestNextPlaylistItemSelectedAfterCompletion()

AddUntilStep("last playlist item selected", () =>
{
var lastItem = this.ChildrenOfType<DrawableRoomPlaylistItem>().Single(p => p.Item.ID == MultiplayerClient.ServerAPIRoom?.Playlist.Last().ID);
var lastItem = this.ChildrenOfType<MultiplayerQueueList>()
.Single()
.ChildrenOfType<DrawableRoomPlaylistItem>()
.Single(p => p.Item.ID == MultiplayerClient.ServerAPIRoom?.Playlist.Last().ID);
return lastItem.IsSelectedItem;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,33 @@ public void TestUserWithMods()
AddStep("set state: locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable()));
}

[Test]
public void TestUserWithStyle()
{
AddStep("add users", () =>
{
MultiplayerClient.AddUser(new APIUser
{
Id = 0,
Username = "User 0",
RulesetsStatistics = new Dictionary<string, UserStatistics>
{
{
Ruleset.Value.ShortName,
new UserStatistics { GlobalRank = RNG.Next(1, 100000), }
}
},
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
});

MultiplayerClient.ChangeUserStyle(0, 259, 2);
});

AddStep("set beatmap locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable()));
AddStep("change user style to beatmap: 258, ruleset: 1", () => MultiplayerClient.ChangeUserStyle(0, 258, 1));
AddStep("change user style to beatmap: null, ruleset: null", () => MultiplayerClient.ChangeUserStyle(0, null, null));
}

[Test]
public void TestModOverlap()
{
Expand Down
15 changes: 15 additions & 0 deletions osu.Game/Localisation/MultiplayerMatchStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ public static class MultiplayerMatchStrings
/// </summary>
public static LocalisableString StartMatchWithCountdown(string humanReadableTime) => new TranslatableString(getKey(@"start_match_width_countdown"), @"Start match in {0}", humanReadableTime);

/// <summary>
/// "Choose the mods which all players should play with."
/// </summary>
public static LocalisableString RequiredModsButtonTooltip => new TranslatableString(getKey(@"required_mods_button_tooltip"), @"Choose the mods which all players should play with.");

/// <summary>
/// "Each player can choose their preferred mods from a selected list."
/// </summary>
public static LocalisableString FreeModsButtonTooltip => new TranslatableString(getKey(@"free_mods_button_tooltip"), @"Each player can choose their preferred mods from a selected list.");

/// <summary>
/// "Each player can choose their preferred difficulty, ruleset and mods."
/// </summary>
public static LocalisableString FreestyleButtonTooltip => new TranslatableString(getKey(@"freestyle_button_tooltip"), @"Each player can choose their preferred difficulty, ruleset and mods.");

private static string getKey(string key) => $@"{prefix}:{key}";
}
}
8 changes: 8 additions & 0 deletions osu.Game/Online/Multiplayer/IMultiplayerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ public interface IMultiplayerClient : IStatefulUserHubClient
/// <param name="beatmapAvailability">The new beatmap availability state of the user.</param>
Task UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability);

/// <summary>
/// Signals that a user in this room changed their style.
/// </summary>
/// <param name="userId">The ID of the user whose style changed.</param>
/// <param name="beatmapId">The user's beatmap.</param>
/// <param name="rulesetId">The user's ruleset.</param>
Task UserStyleChanged(int userId, int? beatmapId, int? rulesetId);

/// <summary>
/// Signals that a user in this room changed their local mods.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ public interface IMultiplayerRoomServer
/// <param name="newBeatmapAvailability">The proposed new beatmap availability state.</param>
Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability);

/// <summary>
/// Change the local user's style in the currently joined room.
/// </summary>
/// <param name="beatmapId">The beatmap.</param>
/// <param name="rulesetId">The ruleset.</param>
Task ChangeUserStyle(int? beatmapId, int? rulesetId);

/// <summary>
/// Change the local user's mods in the currently joined room.
/// </summary>
Expand Down
21 changes: 21 additions & 0 deletions osu.Game/Online/Multiplayer/MultiplayerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@ public async Task ToggleSpectate()

public abstract Task DisconnectInternal();

public abstract Task ChangeUserStyle(int? beatmapId, int? rulesetId);

/// <summary>
/// Change the local user's mods in the currently joined room.
/// </summary>
Expand Down Expand Up @@ -653,6 +655,25 @@ Task IMultiplayerClient.UserBeatmapAvailabilityChanged(int userId, BeatmapAvaila
return Task.CompletedTask;
}

public Task UserStyleChanged(int userId, int? beatmapId, int? rulesetId)
{
Scheduler.Add(() =>
{
var user = Room?.Users.SingleOrDefault(u => u.UserID == userId);

// errors here are not critical - user style is mostly for display.
if (user == null)
return;

user.BeatmapId = beatmapId;
user.RulesetId = rulesetId;

RoomUpdated?.Invoke();
}, false);

return Task.CompletedTask;
}

public Task UserModsChanged(int userId, IEnumerable<APIMod> mods)
{
Scheduler.Add(() =>
Expand Down
18 changes: 15 additions & 3 deletions osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ public class MultiplayerRoomUser : IEquatable<MultiplayerRoomUser>
[Key(1)]
public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle;

[Key(4)]
public MatchUserState? MatchState { get; set; }

/// <summary>
/// The availability state of the current beatmap.
/// </summary>
Expand All @@ -37,6 +34,21 @@ public class MultiplayerRoomUser : IEquatable<MultiplayerRoomUser>
[Key(3)]
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();

[Key(4)]
public MatchUserState? MatchState { get; set; }

/// <summary>
/// If not-null, a local override for this user's ruleset selection.
/// </summary>
[Key(5)]
public int? RulesetId;

/// <summary>
/// If not-null, a local override for this user's beatmap selection.
/// </summary>
[Key(6)]
public int? BeatmapId;

[IgnoreMember]
public APIUser? User { get; set; }

Expand Down
11 changes: 11 additions & 0 deletions osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ private void load(IAPIProvider api)
connection.On(nameof(IMultiplayerClient.GameplayStarted), ((IMultiplayerClient)this).GameplayStarted);
connection.On<GameplayAbortReason>(nameof(IMultiplayerClient.GameplayAborted), ((IMultiplayerClient)this).GameplayAborted);
connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
connection.On<int, int?, int?>(nameof(IMultiplayerClient.UserStyleChanged), ((IMultiplayerClient)this).UserStyleChanged);
connection.On<int, IEnumerable<APIMod>>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
connection.On<int, BeatmapAvailability>(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);
connection.On<MatchRoomState>(nameof(IMultiplayerClient.MatchRoomStateChanged), ((IMultiplayerClient)this).MatchRoomStateChanged);
Expand Down Expand Up @@ -186,6 +187,16 @@ public override Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAva
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability);
}

public override Task ChangeUserStyle(int? beatmapId, int? rulesetId)
{
if (!IsConnected.Value)
return Task.CompletedTask;

Debug.Assert(connection != null);

return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserStyle), beatmapId, rulesetId);
}

public override Task ChangeUserMods(IEnumerable<APIMod> newMods)
{
if (!IsConnected.Value)
Expand Down
1 change: 1 addition & 0 deletions osu.Game/Online/Rooms/CreateRoomScoreRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ protected override WebRequest CreateWebRequest()
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
req.AddParameter("version_hash", versionHash);
req.AddParameter("beatmap_id", beatmapInfo.OnlineID.ToString(CultureInfo.InvariantCulture));
req.AddParameter("beatmap_hash", beatmapInfo.MD5Hash);
req.AddParameter("ruleset_id", rulesetId.ToString(CultureInfo.InvariantCulture));
return req;
Expand Down
6 changes: 6 additions & 0 deletions osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ public class MultiplayerPlaylistItem
[Key(10)]
public double StarRating { get; set; }

/// <summary>
/// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset.
/// </summary>
[Key(11)]
public bool Freestyle { get; set; }

[SerializationConstructor]
public MultiplayerPlaylistItem()
{
Expand Down
11 changes: 7 additions & 4 deletions osu.Game/Online/Rooms/MultiplayerScore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,14 @@ public class MultiplayerScore
[CanBeNull]
public MultiplayerScoresAround ScoresAround { get; set; }

public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap)
[JsonProperty("ruleset_id")]
public int RulesetId { get; set; }

public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, [NotNull] BeatmapInfo beatmap)
{
var ruleset = rulesets.GetRuleset(playlistItem.RulesetID);
var ruleset = rulesets.GetRuleset(RulesetId);
if (ruleset == null)
throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {playlistItem.RulesetID}");
throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {RulesetId}");

var rulesetInstance = ruleset.CreateInstance();

Expand All @@ -91,7 +94,7 @@ public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore ruleset
TotalScore = TotalScore,
MaxCombo = MaxCombo,
BeatmapInfo = beatmap,
Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {playlistItem.RulesetID} not found locally"),
Ruleset = ruleset,
Passed = Passed,
Statistics = Statistics,
MaximumStatistics = MaximumStatistics,
Expand Down
15 changes: 12 additions & 3 deletions osu.Game/Online/Rooms/PlaylistItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ private int onlineBeatmapId
set => Beatmap = new APIBeatmap { OnlineID = value };
}

/// <summary>
/// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset.
/// </summary>
[JsonProperty("freestyle")]
public bool Freestyle { get; set; }

/// <summary>
/// A beatmap representing this playlist item.
/// In many cases, this will *not* contain any usable information apart from OnlineID.
Expand Down Expand Up @@ -101,6 +107,7 @@ public PlaylistItem(MultiplayerPlaylistItem item)
PlayedAt = item.PlayedAt;
RequiredMods = item.RequiredMods.ToArray();
AllowedMods = item.AllowedMods.ToArray();
Freestyle = item.Freestyle;
}

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

#endregion

public PlaylistItem With(Optional<long> id = default, Optional<IBeatmapInfo> beatmap = default, Optional<ushort?> playlistOrder = default)
public PlaylistItem With(Optional<long> id = default, Optional<IBeatmapInfo> beatmap = default, Optional<ushort?> playlistOrder = default, Optional<int> ruleset = default)
{
return new PlaylistItem(beatmap.GetOr(Beatmap))
{
ID = id.GetOr(ID),
OwnerID = OwnerID,
RulesetID = RulesetID,
RulesetID = ruleset.GetOr(RulesetID),
Expired = Expired,
PlaylistOrder = playlistOrder.GetOr(PlaylistOrder),
PlayedAt = PlayedAt,
AllowedMods = AllowedMods,
RequiredMods = RequiredMods,
Freestyle = Freestyle,
valid = { Value = Valid.Value },
};
}
Expand All @@ -143,6 +151,7 @@ public bool Equals(PlaylistItem? other)
&& Expired == other.Expired
&& PlaylistOrder == other.PlaylistOrder
&& AllowedMods.SequenceEqual(other.AllowedMods)
&& RequiredMods.SequenceEqual(other.RequiredMods);
&& RequiredMods.SequenceEqual(other.RequiredMods)
&& Freestyle == other.Freestyle;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ private void load(AudioManager audio)
{
new Drawable[]
{
new DrawableRoomPlaylistItem(playlistItem)
new DrawableRoomPlaylistItem(playlistItem, true)
{
RelativeSizeAxes = Axes.X,
AllowReordering = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,10 @@ public void RefetchScores()

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

userBestScore.Value = req.UserScore;
var userBest = userBestScore.Value?.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo);
var userBest = userBestScore.Value?.CreateScoreInfo(scoreManager, rulesets, beatmap.Value.BeatmapInfo);

cancellationTokenSource?.Cancel();
cancellationTokenSource = null;
Expand Down
6 changes: 4 additions & 2 deletions osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public partial class DrawableRoomPlaylistItem : OsuRearrangeableListItem<Playlis

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

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

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

public DrawableRoomPlaylistItem(PlaylistItem item)
public DrawableRoomPlaylistItem(PlaylistItem item, bool loadImmediately = false)
: base(item)
{
onScreenLoader = new DelayedLoadWrapper(Empty, timeBeforeLoad: loadImmediately ? 0 : 500) { RelativeSizeAxes = Axes.Both };

Item = item;

valid.BindTo(item.Valid);
Expand Down
Loading
Loading