1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-14 05:47:20 +08:00

Merge pull request #31260 from smoogipoo/multiplayer-free-style

Add support for "freestyle" in multiplayer
This commit is contained in:
Dean Herbert 2025-02-04 22:40:38 +09:00 committed by GitHub
commit 15ed029dd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 970 additions and 165 deletions

View File

@ -60,14 +60,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
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);
}

View File

@ -30,6 +30,7 @@ using osu.Game.Screens.OnlinePlay;
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;
@ -271,7 +272,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
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;
});
}

View File

@ -308,6 +308,33 @@ namespace osu.Game.Tests.Visual.Multiplayer
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()
{

View File

@ -24,6 +24,21 @@ namespace osu.Game.Localisation
/// </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}";
}
}

View File

@ -95,6 +95,14 @@ namespace osu.Game.Online.Multiplayer
/// <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>

View File

@ -57,6 +57,13 @@ namespace osu.Game.Online.Multiplayer
/// <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>

View File

@ -358,6 +358,8 @@ namespace osu.Game.Online.Multiplayer
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>
@ -653,6 +655,25 @@ namespace osu.Game.Online.Multiplayer
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(() =>

View File

@ -22,9 +22,6 @@ namespace osu.Game.Online.Multiplayer
[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>
@ -37,6 +34,21 @@ namespace osu.Game.Online.Multiplayer
[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; }

View File

@ -60,6 +60,7 @@ namespace osu.Game.Online.Multiplayer
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);
@ -186,6 +187,16 @@ namespace osu.Game.Online.Multiplayer
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)

View File

@ -31,6 +31,7 @@ namespace osu.Game.Online.Rooms
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;

View File

@ -56,6 +56,12 @@ namespace osu.Game.Online.Rooms
[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()
{

View File

@ -77,11 +77,14 @@ namespace osu.Game.Online.Rooms
[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();
@ -91,7 +94,7 @@ namespace osu.Game.Online.Rooms
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,

View File

@ -67,6 +67,12 @@ namespace osu.Game.Online.Rooms
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.
@ -101,6 +107,7 @@ namespace osu.Game.Online.Rooms
PlayedAt = item.PlayedAt;
RequiredMods = item.RequiredMods.ToArray();
AllowedMods = item.AllowedMods.ToArray();
Freestyle = item.Freestyle;
}
public void MarkInvalid() => valid.Value = false;
@ -120,18 +127,19 @@ namespace osu.Game.Online.Rooms
#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 },
};
}
@ -143,6 +151,7 @@ namespace osu.Game.Online.Rooms
&& Expired == other.Expired
&& PlaylistOrder == other.PlaylistOrder
&& AllowedMods.SequenceEqual(other.AllowedMods)
&& RequiredMods.SequenceEqual(other.RequiredMods);
&& RequiredMods.SequenceEqual(other.RequiredMods)
&& Freestyle == other.Freestyle;
}
}

View File

@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
new Drawable[]
{
new DrawableRoomPlaylistItem(playlistItem)
new DrawableRoomPlaylistItem(playlistItem, true)
{
RelativeSizeAxes = Axes.X,
AllowReordering = false,

View File

@ -142,10 +142,10 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
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;

View File

@ -74,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay
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;
@ -120,9 +120,11 @@ namespace osu.Game.Screens.OnlinePlay
[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);

View File

@ -18,6 +18,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Screens.OnlinePlay
{
@ -36,8 +37,9 @@ namespace osu.Game.Screens.OnlinePlay
}
}
private OsuSpriteText count = null!;
public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); }
private OsuSpriteText count = null!;
private Circle circle = null!;
private readonly FreeModSelectOverlay freeModSelectOverlay;
@ -45,6 +47,9 @@ namespace osu.Game.Screens.OnlinePlay
public FooterButtonFreeMods(FreeModSelectOverlay freeModSelectOverlay)
{
this.freeModSelectOverlay = freeModSelectOverlay;
// Overwrite any external behaviour as we delegate the main toggle action to a sub-button.
base.Action = toggleAllFreeMods;
}
[Resolved]
@ -91,6 +96,8 @@ namespace osu.Game.Screens.OnlinePlay
SelectedColour = colours.Yellow;
DeselectedColour = SelectedColour.Opacity(0.5f);
Text = @"freemods";
TooltipText = MultiplayerMatchStrings.FreeModsButtonTooltip;
}
protected override void LoadComplete()
@ -98,9 +105,6 @@ namespace osu.Game.Screens.OnlinePlay
base.LoadComplete();
Current.BindValueChanged(_ => updateModDisplay(), true);
// Overwrite any external behaviour as we delegate the main toggle action to a sub-button.
Action = toggleAllFreeMods;
}
/// <summary>

View File

@ -0,0 +1,103 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Select;
using osu.Game.Localisation;
namespace osu.Game.Screens.OnlinePlay
{
public partial class FooterButtonFreestyle : FooterButton, IHasCurrentValue<bool>
{
private readonly BindableWithCurrent<bool> current = new BindableWithCurrent<bool>();
public Bindable<bool> Current
{
get => current.Current;
set => current.Current = value;
}
public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); }
private OsuSpriteText text = null!;
private Circle circle = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
public FooterButtonFreestyle()
{
// Overwrite any external behaviour as we delegate the main toggle action to a sub-button.
base.Action = () => current.Value = !current.Value;
}
[BackgroundDependencyLoader]
private void load()
{
ButtonContentContainer.AddRange(new[]
{
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
circle = new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = colours.YellowDark,
RelativeSizeAxes = Axes.Both,
},
text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding(5),
UseFullGlyphHeight = false,
}
}
}
});
SelectedColour = colours.Yellow;
DeselectedColour = SelectedColour.Opacity(0.5f);
Text = @"freestyle";
TooltipText = MultiplayerMatchStrings.FreestyleButtonTooltip;
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(_ => updateDisplay(), true);
}
private void updateDisplay()
{
if (current.Value)
{
text.Text = "on";
text.FadeColour(colours.Gray2, 200, Easing.OutQuint);
circle.FadeColour(colours.Yellow, 200, Easing.OutQuint);
}
else
{
text.Text = "off";
text.FadeColour(colours.GrayF, 200, Easing.OutQuint);
circle.FadeColour(colours.Gray4, 200, Easing.OutQuint);
}
}
}
}

View File

@ -179,6 +179,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
new FreestyleStatusPill(Room)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
endDateInfo = new EndDateInfo(Room)
{
Anchor = Anchor.CentreLeft,

View File

@ -0,0 +1,64 @@
// 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.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public partial class FreestyleStatusPill : OnlinePlayPill
{
private readonly Room room;
[Resolved]
private OsuColour colours { get; set; } = null!;
protected override FontUsage Font => base.Font.With(weight: FontWeight.SemiBold);
public FreestyleStatusPill(Room room)
{
this.room = room;
}
protected override void LoadComplete()
{
base.LoadComplete();
Pill.Background.Alpha = 1;
Pill.Background.Colour = colours.Yellow;
TextFlow.Text = "Freestyle";
TextFlow.Colour = Color4.Black;
room.PropertyChanged += onRoomPropertyChanged;
updateFreestyleStatus();
}
private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(Room.CurrentPlaylistItem):
case nameof(Room.Playlist):
updateFreestyleStatus();
break;
}
}
private void updateFreestyleStatus()
{
PlaylistItem? currentItem = room.Playlist.GetCurrentItem() ?? room.CurrentPlaylistItem;
Alpha = currentItem?.Freestyle == true ? 1 : 0;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
room.PropertyChanged -= onRoomPropertyChanged;
}
}
}

View File

@ -28,6 +28,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Utils;
using Container = osu.Framework.Graphics.Containers.Container;
namespace osu.Game.Screens.OnlinePlay.Match
@ -50,7 +51,18 @@ namespace osu.Game.Screens.OnlinePlay.Match
/// A container that provides controls for selection of user mods.
/// This will be shown/hidden automatically when applicable.
/// </summary>
protected Drawable? UserModsSection;
protected Drawable UserModsSection = null!;
/// <summary>
/// A container that provides controls for selection of the user style.
/// This will be shown/hidden automatically when applicable.
/// </summary>
protected Drawable UserStyleSection = null!;
/// <summary>
/// A container that will display the user's style.
/// </summary>
protected Container<DrawableRoomPlaylistItem> UserStyleDisplayContainer = null!;
private Sample? sampleStart;
@ -254,11 +266,11 @@ namespace osu.Game.Screens.OnlinePlay.Match
{
base.LoadComplete();
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged));
UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods));
SelectedItem.BindValueChanged(_ => updateSpecifics());
UserMods.BindValueChanged(_ => updateSpecifics());
beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem);
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateWorkingBeatmap());
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics());
userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(UserModsSelectOverlay);
@ -327,7 +339,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
public override void OnSuspending(ScreenTransitionEvent e)
{
// Should be a noop in most cases, but let's ensure beyond doubt that the beatmap is in a correct state.
updateWorkingBeatmap();
updateSpecifics();
onLeaving();
base.OnSuspending(e);
@ -336,10 +348,10 @@ namespace osu.Game.Screens.OnlinePlay.Match
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(e);
updateWorkingBeatmap();
updateSpecifics();
beginHandlingTrack();
Scheduler.AddOnce(UpdateMods);
Scheduler.AddOnce(updateRuleset);
}
protected bool ExitConfirmed { get; private set; }
@ -389,9 +401,13 @@ namespace osu.Game.Screens.OnlinePlay.Match
protected void StartPlay()
{
if (SelectedItem.Value == null)
if (SelectedItem.Value is not PlaylistItem item)
return;
item = item.With(
ruleset: GetGameplayRuleset().OnlineID,
beatmap: new Optional<IBeatmapInfo>(GetGameplayBeatmap()));
// User may be at song select or otherwise when the host starts gameplay.
// Ensure that they first return to this screen, else global bindables (beatmap etc.) may be in a bad state.
if (!this.IsCurrentScreen())
@ -407,7 +423,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
// fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes).
var targetScreen = (Screen?)ParentScreen ?? this;
targetScreen.Push(CreateGameplayScreen(SelectedItem.Value));
targetScreen.Push(CreateGameplayScreen(item));
}
/// <summary>
@ -417,66 +433,75 @@ namespace osu.Game.Screens.OnlinePlay.Match
/// <returns>The screen to enter.</returns>
protected abstract Screen CreateGameplayScreen(PlaylistItem selectedItem);
private void selectedItemChanged()
private void updateSpecifics()
{
updateWorkingBeatmap();
if (SelectedItem.Value is not PlaylistItem selected)
if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item)
return;
var rulesetInstance = Rulesets.GetRuleset(selected.RulesetID)?.CreateInstance();
Debug.Assert(rulesetInstance != null);
var allowedMods = selected.AllowedMods.Select(m => m.ToMod(rulesetInstance));
var rulesetInstance = GetGameplayRuleset().CreateInstance();
// Remove any user mods that are no longer allowed.
UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList();
Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray();
if (!newUserMods.SequenceEqual(UserMods.Value))
UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList();
UpdateMods();
updateRuleset();
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
int beatmapId = GetGameplayBeatmap().OnlineID;
var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId);
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
UserModsSelectOverlay.Beatmap.Value = Beatmap.Value;
if (!selected.AllowedMods.Any())
Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray();
Ruleset.Value = GetGameplayRuleset();
bool freeMod = item.AllowedMods.Any();
bool freestyle = item.Freestyle;
// For now, the game can never be in a state where freemod and freestyle are on at the same time.
// This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert.
Debug.Assert(!freeMod || !freestyle);
if (freeMod)
{
UserModsSection?.Hide();
UserModsSelectOverlay.Hide();
UserModsSelectOverlay.IsValidMod = _ => false;
UserModsSection.Show();
UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
}
else
{
UserModsSection?.Show();
UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
UserModsSection.Hide();
UserModsSelectOverlay.Hide();
UserModsSelectOverlay.IsValidMod = _ => false;
}
if (freestyle)
{
UserStyleSection.Show();
PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional<IBeatmapInfo>(GetGameplayBeatmap()));
PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item;
if (gameplayItem.Equals(currentItem))
return;
UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true)
{
AllowReordering = false,
AllowEditing = freestyle,
RequestEdit = _ => OpenStyleSelection()
};
}
else
UserStyleSection.Hide();
}
private void updateWorkingBeatmap()
{
if (SelectedItem.Value == null || !this.IsCurrentScreen())
return;
protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray();
var beatmap = SelectedItem.Value?.Beatmap;
protected virtual RulesetInfo GetGameplayRuleset() => Rulesets.GetRuleset(SelectedItem.Value!.RulesetID)!;
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID);
protected virtual IBeatmapInfo GetGameplayBeatmap() => SelectedItem.Value!.Beatmap;
UserModsSelectOverlay.Beatmap.Value = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
}
protected virtual void UpdateMods()
{
if (SelectedItem.Value == null || !this.IsCurrentScreen())
return;
var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance();
Debug.Assert(rulesetInstance != null);
Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList();
}
private void updateRuleset()
{
if (SelectedItem.Value == null || !this.IsCurrentScreen())
return;
Ruleset.Value = Rulesets.GetRuleset(SelectedItem.Value.RulesetID);
}
protected abstract void OpenStyleSelection();
private void beginHandlingTrack()
{

View File

@ -0,0 +1,89 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public partial class MultiplayerMatchFreestyleSelect : OnlinePlayFreestyleSelect
{
[Resolved]
private MultiplayerClient client { get; set; } = null!;
[Resolved]
private OngoingOperationTracker operationTracker { get; set; } = null!;
private readonly IBindable<bool> operationInProgress = new Bindable<bool>();
private LoadingLayer loadingLayer = null!;
private IDisposable? selectionOperation;
public MultiplayerMatchFreestyleSelect(Room room, PlaylistItem item)
: base(room, item)
{
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(loadingLayer = new LoadingLayer(true));
}
protected override void LoadComplete()
{
base.LoadComplete();
operationInProgress.BindTo(operationTracker.InProgress);
operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true);
}
private void updateLoadingLayer()
{
if (operationInProgress.Value)
loadingLayer.Show();
else
loadingLayer.Hide();
}
protected override bool OnStart()
{
if (operationInProgress.Value)
{
Logger.Log($"{nameof(OnStart)} aborted due to {nameof(operationInProgress)}");
return false;
}
selectionOperation = operationTracker.BeginOperation();
client.ChangeUserStyle(Beatmap.Value.BeatmapInfo.OnlineID, Ruleset.Value.OnlineID)
.FireAndForget(onSuccess: () =>
{
selectionOperation.Dispose();
Schedule(() =>
{
// If an error or server side trigger occurred this screen may have already exited by external means.
if (this.IsCurrentScreen())
this.Exit();
});
}, onError: _ =>
{
selectionOperation.Dispose();
Schedule(() =>
{
Carousel.AllowSelection = true;
});
});
return true;
}
}
}

View File

@ -86,7 +86,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
BeatmapChecksum = item.Beatmap.MD5Hash,
RulesetID = item.RulesetID,
RequiredMods = item.RequiredMods.ToArray(),
AllowedMods = item.AllowedMods.ToArray()
AllowedMods = item.AllowedMods.ToArray(),
Freestyle = item.Freestyle
};
Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem);

View File

@ -16,6 +16,8 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
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.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
@ -145,43 +147,66 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
SelectedItem = SelectedItem
}
},
new[]
new Drawable[]
{
UserModsSection = new FillFlowContainer
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 10 },
Alpha = 0,
Children = new Drawable[]
Children = new[]
{
new OverlinedHeader("Extra mods"),
new FillFlowContainer
UserModsSection = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Children = new Drawable[]
{
new UserModSelectButton
new OverlinedHeader("Extra mods"),
new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new UserModSelectButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
},
}
},
UserStyleSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Children = new Drawable[]
{
new OverlinedHeader("Difficulty"),
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
},
}
},
}
},
},
RowDimensions = new[]
@ -228,6 +253,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit));
}
protected override void OpenStyleSelection()
{
if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item)
return;
this.Push(new MultiplayerMatchFreestyleSelect(Room, item));
}
protected override Drawable CreateFooter() => new MultiplayerMatchFooter
{
SelectedItem = SelectedItem
@ -238,16 +271,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
SelectedItem = SelectedItem
};
protected override void UpdateMods()
protected override APIMod[] GetGameplayMods()
{
if (SelectedItem.Value == null || client.LocalUser == null || !this.IsCurrentScreen())
return;
// Using the room's reported status makes the server authoritative.
return client.LocalUser?.Mods != null ? client.LocalUser.Mods.Concat(SelectedItem.Value!.RequiredMods).ToArray() : base.GetGameplayMods();
}
// update local mods based on room's reported status for the local user (omitting the base call implementation).
// this makes the server authoritative, and avoids the local user potentially setting mods that the server is not aware of (ie. if the match was started during the selection being changed).
var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance();
Debug.Assert(rulesetInstance != null);
Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(rulesetInstance)).Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList();
protected override RulesetInfo GetGameplayRuleset()
{
// Using the room's reported status makes the server authoritative.
return client.LocalUser?.RulesetId != null ? Rulesets.GetRuleset(client.LocalUser.RulesetId.Value)! : base.GetGameplayRuleset();
}
protected override IBeatmapInfo GetGameplayBeatmap()
{
// Using the room's reported status makes the server authoritative.
return client.LocalUser?.BeatmapId != null ? new APIBeatmap { OnlineID = client.LocalUser.BeatmapId.Value } : base.GetGameplayBeatmap();
}
[Resolved(canBeNull: true)]
@ -349,23 +388,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return;
}
updateCurrentItem();
SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId);
addItemButton.Alpha = localUserCanAddItem ? 1 : 0;
Scheduler.AddOnce(UpdateMods);
Activity.Value = new UserActivity.InLobby(Room);
}
private bool localUserCanAddItem => client.IsHost || Room.QueueMode != QueueMode.HostOnly;
private void updateCurrentItem()
{
Debug.Assert(client.Room != null);
SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId);
}
private void handleRoomLost() => Schedule(() =>
{
Logger.Log($"{this} exiting due to loss of room or connection");

View File

@ -4,6 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
@ -14,6 +16,9 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Logging;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@ -47,6 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
private SpriteIcon crown = null!;
private OsuSpriteText userRankText = null!;
private StyleDisplayIcon userStyleDisplay = null!;
private ModDisplay userModsDisplay = null!;
private StateDisplay userStateDisplay = null!;
@ -149,16 +155,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
}
}
},
new Container
new FillFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Right = 70 },
Child = userModsDisplay = new ModDisplay
Children = new Drawable[]
{
Scale = new Vector2(0.5f),
ExpansionMode = ExpansionMode.AlwaysContracted,
userStyleDisplay = new StyleDisplayIcon(),
userModsDisplay = new ModDisplay
{
Scale = new Vector2(0.5f),
ExpansionMode = ExpansionMode.AlwaysContracted,
}
}
},
userStateDisplay = new StateDisplay
@ -208,9 +218,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating))
{
userModsDisplay.FadeIn(fade_time);
userStyleDisplay.FadeIn(fade_time);
}
else
{
userModsDisplay.FadeOut(fade_time);
userStyleDisplay.FadeOut(fade_time);
}
if ((User.BeatmapId == null && User.RulesetId == null) || (User.BeatmapId == currentItem?.BeatmapID && User.RulesetId == currentItem?.RulesetID))
userStyleDisplay.Style = null;
else
userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0);
kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0;
crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0;
@ -284,5 +305,81 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
IconHoverColour = colours.Red;
}
}
private partial class StyleDisplayIcon : CompositeComponent
{
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
public StyleDisplayIcon()
{
AutoSizeAxes = Axes.Both;
}
private (int beatmap, int ruleset)? style;
public (int beatmap, int ruleset)? Style
{
get => style;
set
{
if (style == value)
return;
style = value;
Scheduler.Add(refresh);
}
}
private CancellationTokenSource? cancellationSource;
private void refresh()
{
cancellationSource?.Cancel();
cancellationSource?.Dispose();
cancellationSource = null;
if (Style == null)
{
ClearInternal();
return;
}
cancellationSource = new CancellationTokenSource();
CancellationToken token = cancellationSource.Token;
int localBeatmap = Style.Value.beatmap;
int localRuleset = Style.Value.ruleset;
Task.Run(async () =>
{
try
{
var beatmap = await beatmapLookupCache.GetBeatmapAsync(localBeatmap, token).ConfigureAwait(false);
if (beatmap == null)
return;
Schedule(() =>
{
if (token.IsCancellationRequested)
return;
InternalChild = new DifficultyIcon(beatmap, rulesets.GetRuleset(localRuleset))
{
Size = new Vector2(20),
TooltipType = DifficultyIconTooltipType.Extended,
};
});
}
catch (Exception e)
{
Logger.Log($"Error while populating participant style icon {e}");
}
}, token);
}
}
}
}

View File

@ -0,0 +1,104 @@
// 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.Collections.Generic;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.Select;
using osu.Game.Users;
namespace osu.Game.Screens.OnlinePlay
{
public abstract partial class OnlinePlayFreestyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap
{
public string ShortTitle => "style selection";
public override string Title => ShortTitle.Humanize();
public override bool AllowEditing => false;
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
private readonly Room room;
private readonly PlaylistItem item;
protected OnlinePlayFreestyleSelect(Room room, PlaylistItem item)
{
this.room = room;
this.item = item;
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
}
[BackgroundDependencyLoader]
private void load()
{
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
}
protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item);
protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons()
{
// Required to create the drawable components.
base.CreateSongSelectFooterButtons();
return Enumerable.Empty<(FooterButton, OverlayContainer?)>();
}
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
{
// This screen cannot present beatmaps.
}
private partial class DifficultySelectFilterControl : FilterControl
{
private readonly PlaylistItem item;
private double itemLength;
private int beatmapSetId;
public DifficultySelectFilterControl(PlaylistItem item)
{
this.item = item;
}
[BackgroundDependencyLoader]
private void load(RealmAccess realm)
{
realm.Run(r =>
{
int beatmapId = item.Beatmap.OnlineID;
BeatmapInfo? beatmap = r.All<BeatmapInfo>().FirstOrDefault(b => b.OnlineID == beatmapId);
itemLength = beatmap?.Length ?? 0;
beatmapSetId = beatmap?.BeatmapSet?.OnlineID ?? 0;
});
}
public override FilterCriteria CreateCriteria()
{
var criteria = base.CreateCriteria();
// Must be from the same set as the playlist item.
criteria.BeatmapSetId = beatmapSetId;
criteria.HasOnlineID = true;
// Must be within 30s of the playlist item.
criteria.Length.Min = itemLength - 30000;
criteria.Length.Max = itemLength + 30000;
criteria.Length.IsLowerInclusive = true;
criteria.Length.IsUpperInclusive = true;
return criteria;
}
}
}
}

View File

@ -21,6 +21,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osu.Game.Users;
using osu.Game.Utils;
using osu.Game.Localisation;
namespace osu.Game.Screens.OnlinePlay
{
@ -41,10 +42,12 @@ namespace osu.Game.Screens.OnlinePlay
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
protected readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
protected readonly Bindable<bool> Freestyle = new Bindable<bool>();
private readonly Room room;
private readonly PlaylistItem? initialItem;
private readonly FreeModSelectOverlay freeModSelectOverlay;
private readonly FreeModSelectOverlay freeModSelect;
private FooterButton freeModsFooterButton = null!;
private IDisposable? freeModSelectOverlayRegistration;
@ -61,7 +64,7 @@ namespace osu.Game.Screens.OnlinePlay
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
freeModSelectOverlay = new FreeModSelectOverlay
freeModSelect = new FreeModSelectOverlay
{
SelectedMods = { BindTarget = FreeMods },
IsValidMod = IsValidFreeMod,
@ -72,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay
private void load()
{
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
LoadComponent(freeModSelectOverlay);
LoadComponent(freeModSelect);
}
protected override void LoadComplete()
@ -108,12 +111,35 @@ namespace osu.Game.Screens.OnlinePlay
Mods.Value = initialItem.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
}
Freestyle.Value = initialItem.Freestyle;
}
Mods.BindValueChanged(onModsChanged);
Ruleset.BindValueChanged(onRulesetChanged);
Freestyle.BindValueChanged(onFreestyleChanged, true);
freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelectOverlay);
freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect);
}
private void onFreestyleChanged(ValueChangedEvent<bool> enabled)
{
if (enabled.NewValue)
{
freeModsFooterButton.Enabled.Value = false;
ModsFooterButton.Enabled.Value = false;
ModSelect.Hide();
freeModSelect.Hide();
Mods.Value = [];
FreeMods.Value = [];
}
else
{
freeModsFooterButton.Enabled.Value = true;
ModsFooterButton.Enabled.Value = true;
}
}
private void onModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
@ -121,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay
FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList();
// Reset the validity delegate to update the overlay's display.
freeModSelectOverlay.IsValidMod = IsValidFreeMod;
freeModSelect.IsValidMod = IsValidFreeMod;
}
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset)
@ -135,7 +161,8 @@ namespace osu.Game.Screens.OnlinePlay
{
RulesetID = Ruleset.Value.OnlineID,
RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(),
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray()
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(),
Freestyle = Freestyle.Value
};
return SelectItem(item);
@ -150,9 +177,9 @@ namespace osu.Game.Screens.OnlinePlay
public override bool OnBackButton()
{
if (freeModSelectOverlay.State.Value == Visibility.Visible)
if (freeModSelect.State.Value == Visibility.Visible)
{
freeModSelectOverlay.Hide();
freeModSelect.Hide();
return true;
}
@ -161,7 +188,7 @@ namespace osu.Game.Screens.OnlinePlay
public override bool OnExiting(ScreenExitEvent e)
{
freeModSelectOverlay.Hide();
freeModSelect.Hide();
return base.OnExiting(e);
}
@ -170,12 +197,17 @@ namespace osu.Game.Screens.OnlinePlay
IsValidMod = IsValidMod
};
protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons()
protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons()
{
var baseButtons = base.CreateSongSelectFooterButtons().ToList();
var freeModsButton = new FooterButtonFreeMods(freeModSelectOverlay) { Current = FreeMods };
baseButtons.Insert(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (freeModsButton, freeModSelectOverlay));
baseButtons.Single(i => i.button is FooterButtonMods).button.TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip;
baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[]
{
(freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }, null),
(new FooterButtonFreestyle { Current = Freestyle }, null)
});
return baseButtons;
}

View File

@ -189,7 +189,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
/// <param name="pivot">An optional pivot around which the scores were retrieved.</param>
protected virtual ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
{
var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
// Invoke callback to add the scores. Exclude the score provided to this screen since it's added already.
callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID));

View File

@ -0,0 +1,37 @@
// 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 osu.Framework.Bindables;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
public partial class PlaylistsRoomFreestyleSelect : OnlinePlayFreestyleSelect
{
public new readonly Bindable<BeatmapInfo?> Beatmap = new Bindable<BeatmapInfo?>();
public new readonly Bindable<RulesetInfo?> Ruleset = new Bindable<RulesetInfo?>();
public PlaylistsRoomFreestyleSelect(Room room, PlaylistItem item)
: base(room, item)
{
}
protected override bool OnStart()
{
// Beatmaps without a valid online ID are filtered away; this is just a final safety.
if (base.Beatmap.Value.BeatmapInfo.OnlineID < 0)
return false;
if (base.Ruleset.Value.OnlineID < 0)
return false;
Beatmap.Value = base.Beatmap.Value.BeatmapInfo;
Ruleset.Value = base.Ruleset.Value;
this.Exit();
return true;
}
}
}

View File

@ -11,11 +11,13 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Cursor;
using osu.Game.Input;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Match.Components;
@ -46,6 +48,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private FillFlowContainer progressSection = null!;
private DrawableRoomPlaylist drawablePlaylist = null!;
private readonly Bindable<BeatmapInfo?> userBeatmap = new Bindable<BeatmapInfo?>();
private readonly Bindable<RulesetInfo?> userRuleset = new Bindable<RulesetInfo?>();
public PlaylistsRoomSubScreen(Room room)
: base(room, false) // Editing is temporarily not allowed.
{
@ -66,6 +71,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
base.LoadComplete();
SelectedItem.BindValueChanged(onSelectedItemChanged, true);
isIdle.BindValueChanged(_ => updatePollingRate(), true);
Room.PropertyChanged += onRoomPropertyChanged;
@ -74,6 +80,16 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
updateRoomPlaylist();
}
private void onSelectedItemChanged(ValueChangedEvent<PlaylistItem?> item)
{
// Simplest for now.
userBeatmap.Value = null;
userRuleset.Value = null;
}
protected override IBeatmapInfo GetGameplayBeatmap() => userBeatmap.Value ?? base.GetGameplayBeatmap();
protected override RulesetInfo GetGameplayRuleset() => userRuleset.Value ?? base.GetGameplayRuleset();
private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
@ -168,41 +184,65 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new[]
new Drawable[]
{
UserModsSection = new FillFlowContainer
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Margin = new MarginPadding { Bottom = 10 },
Children = new Drawable[]
Children = new[]
{
new OverlinedHeader("Extra mods"),
new FillFlowContainer
UserModsSection = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Margin = new MarginPadding { Bottom = 10 },
Children = new Drawable[]
{
new UserModSelectButton
new OverlinedHeader("Extra mods"),
new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
},
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new UserModSelectButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
}
}
}
},
UserStyleSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Children = new Drawable[]
{
new OverlinedHeader("Difficulty"),
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
},
}
},
},
@ -274,6 +314,18 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
},
};
protected override void OpenStyleSelection()
{
if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item)
return;
this.Push(new PlaylistsRoomFreestyleSelect(Room, item)
{
Beatmap = { BindTarget = userBeatmap },
Ruleset = { BindTarget = userRuleset }
});
}
private void updatePollingRate()
{
selectionPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000;

View File

@ -39,7 +39,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
ID = room.Playlist.Count == 0 ? 0 : room.Playlist.Max(p => p.ID) + 1,
RulesetID = Ruleset.Value.OnlineID,
RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(),
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray()
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(),
Freestyle = Freestyle.Value
};
}
}

View File

@ -90,6 +90,12 @@ namespace osu.Game.Screens.Select.Carousel
if (match && criteria.RulesetCriteria != null)
match &= criteria.RulesetCriteria.Matches(BeatmapInfo, criteria);
if (match && criteria.HasOnlineID == true)
match &= BeatmapInfo.OnlineID >= 0;
if (match && criteria.BeatmapSetId != null)
match &= criteria.BeatmapSetId == BeatmapInfo.BeatmapSet?.OnlineID;
return match;
}

View File

@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select
[CanBeNull]
private FilterCriteria currentCriteria;
public FilterCriteria CreateCriteria()
public virtual FilterCriteria CreateCriteria()
{
string query = searchTextBox.Text;

View File

@ -56,6 +56,9 @@ namespace osu.Game.Screens.Select
public RulesetInfo? Ruleset;
public IReadOnlyList<Mod>? Mods;
public bool AllowConvertedBeatmaps;
public int? BeatmapSetId;
public bool? HasOnlineID;
private string searchText = string.Empty;

View File

@ -83,6 +83,11 @@ namespace osu.Game.Screens.Select
/// </summary>
protected Container FooterPanels { get; private set; } = null!;
/// <summary>
/// The <see cref="FooterButton"/> that opens the mod select dialog.
/// </summary>
protected FooterButton ModsFooterButton { get; private set; } = null!;
/// <summary>
/// Whether entering editor mode should be allowed.
/// </summary>
@ -214,11 +219,11 @@ namespace osu.Game.Screens.Select
},
}
},
FilterControl = new FilterControl
FilterControl = CreateFilterControl().With(d =>
{
RelativeSizeAxes = Axes.X,
Height = FilterControl.HEIGHT,
},
d.RelativeSizeAxes = Axes.X;
d.Height = FilterControl.HEIGHT;
}),
new GridContainer // used for max width implementation
{
RelativeSizeAxes = Axes.Both,
@ -387,6 +392,8 @@ namespace osu.Game.Screens.Select
SampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection");
}
protected virtual FilterControl CreateFilterControl() => new FilterControl();
protected override void LoadComplete()
{
base.LoadComplete();
@ -408,9 +415,9 @@ namespace osu.Game.Screens.Select
/// Creates the buttons to be displayed in the footer.
/// </summary>
/// <returns>A set of <see cref="FooterButton"/> and an optional <see cref="OverlayContainer"/> which the button opens when pressed.</returns>
protected virtual IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() => new (FooterButton, OverlayContainer?)[]
protected virtual IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() => new (FooterButton, OverlayContainer?)[]
{
(new FooterButtonMods { Current = Mods }, ModSelect),
(ModsFooterButton = new FooterButtonMods { Current = Mods }, ModSelect),
(new FooterButtonRandom
{
NextRandom = () => Carousel.SelectNextRandom(),

View File

@ -335,6 +335,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
return Task.CompletedTask;
}
public override Task ChangeUserStyle(int? beatmapId, int? rulesetId)
{
ChangeUserStyle(api.LocalUser.Value.Id, beatmapId, rulesetId);
return Task.CompletedTask;
}
public void ChangeUserStyle(int userId, int? beatmapId, int? rulesetId)
{
Debug.Assert(ServerRoom != null);
var user = ServerRoom.Users.Single(u => u.UserID == userId);
user.BeatmapId = beatmapId;
user.RulesetId = rulesetId;
((IMultiplayerClient)this).UserStyleChanged(userId, beatmapId, rulesetId);
}
public void ChangeUserMods(int userId, IEnumerable<Mod> newMods)
=> ChangeUserMods(userId, newMods.Select(m => new APIMod(m)));