1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 03:27:24 +08:00

Merge pull request #11641 from smoogipoo/freemods

Add support for optional per-user mods in multiplayer (aka freemod)
This commit is contained in:
Dean Herbert 2021-02-05 13:59:30 +09:00 committed by GitHub
commit 9258836f10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 558 additions and 138 deletions

View File

@ -8,6 +8,8 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
using osu.Game.Users;
using osuTK;
@ -123,5 +125,32 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
}
[Test]
public void TestUserWithMods()
{
AddStep("add user", () =>
{
Client.AddUser(new User
{
Id = 0,
Username = "User 0",
CurrentModeRank = RNG.Next(1, 100000),
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
});
Client.ChangeUserMods(0, new Mod[]
{
new OsuModHardRock(),
new OsuModDifficultyAdjust { ApproachRate = { Value = 1 } }
});
});
for (var i = MultiplayerUserState.Idle; i < MultiplayerUserState.Results; i++)
{
var state = i;
AddStep($"set state: {state}", () => Client.ChangeUserState(0, state));
}
}
}
}

View File

@ -19,11 +19,11 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.Select;
using osu.Game.Screens.OnlinePlay.Playlists;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchSongSelect : RoomTestScene
public class TestScenePlaylistsSongSelect : RoomTestScene
{
[Resolved]
private BeatmapManager beatmapManager { get; set; }
@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private RulesetStore rulesets;
private TestMatchSongSelect songSelect;
private TestPlaylistsSongSelect songSelect;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Beatmap.SetDefault();
});
AddStep("create song select", () => LoadScreen(songSelect = new TestMatchSongSelect()));
AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect()));
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen());
}
@ -176,7 +176,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value));
}
private class TestMatchSongSelect : MatchSongSelect
private class TestPlaylistsSongSelect : PlaylistsSongSelect
{
public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails;
}

View File

@ -1,7 +1,9 @@
// 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.Threading.Tasks;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
namespace osu.Game.Online.Multiplayer
@ -55,6 +57,13 @@ 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 local mods.
/// </summary>
/// <param name="userId">The ID of the user whose mods have changed.</param>
/// <param name="mods">The user's new local mods.</param>
Task UserModsChanged(int userId, IEnumerable<APIMod> mods);
/// <summary>
/// Signals that a match is to be started. This will *only* be sent to clients which are to begin loading at this point.
/// </summary>

View File

@ -1,7 +1,9 @@
// 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.Threading.Tasks;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
namespace osu.Game.Online.Multiplayer
@ -47,6 +49,12 @@ 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 mods in the currently joined room.
/// </summary>
/// <param name="newMods">The proposed new mods, excluding any required by the room itself.</param>
Task ChangeUserMods(IEnumerable<APIMod> newMods);
/// <summary>
/// As the host of a room, start the match.
/// </summary>

View File

@ -4,6 +4,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
@ -91,6 +92,7 @@ namespace osu.Game.Online.Multiplayer
connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested);
connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted);
connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
connection.On<int, IEnumerable<APIMod>>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
connection.Closed += async ex =>
{
@ -189,6 +191,14 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability);
}
public override Task ChangeUserMods(IEnumerable<APIMod> newMods)
{
if (!isConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserMods), newMods);
}
public override Task StartMatch()
{
if (!isConnected.Value)

View File

@ -30,15 +30,24 @@ namespace osu.Game.Online.Multiplayer
[NotNull]
[Key(4)]
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
public IEnumerable<APIMod> RequiredMods { get; set; } = Enumerable.Empty<APIMod>();
[NotNull]
[Key(5)]
public IEnumerable<APIMod> AllowedMods { get; set; } = Enumerable.Empty<APIMod>();
public bool Equals(MultiplayerRoomSettings other)
=> BeatmapID == other.BeatmapID
&& BeatmapChecksum == other.BeatmapChecksum
&& Mods.SequenceEqual(other.Mods)
&& RequiredMods.SequenceEqual(other.RequiredMods)
&& AllowedMods.SequenceEqual(other.AllowedMods)
&& RulesetID == other.RulesetID
&& Name.Equals(other.Name, StringComparison.Ordinal);
public override string ToString() => $"Name:{Name} Beatmap:{BeatmapID} ({BeatmapChecksum}) Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}";
public override string ToString() => $"Name:{Name}"
+ $" Beatmap:{BeatmapID} ({BeatmapChecksum})"
+ $" RequiredMods:{string.Join(',', RequiredMods)}"
+ $" AllowedMods:{string.Join(',', AllowedMods)}"
+ $" Ruleset:{RulesetID}";
}
}

View File

@ -4,8 +4,12 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using MessagePack;
using Newtonsoft.Json;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Users;
@ -27,6 +31,13 @@ namespace osu.Game.Online.Multiplayer
[Key(2)]
public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.LocallyAvailable();
/// <summary>
/// Any mods applicable only to the local user.
/// </summary>
[Key(3)]
[NotNull]
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
[IgnoreMember]
public User? User { get; set; }

View File

@ -21,6 +21,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Users;
using osu.Game.Utils;
@ -191,7 +192,8 @@ namespace osu.Game.Online.Multiplayer
BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID,
BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash,
RulesetID = item.GetOr(existingPlaylistItem).RulesetID,
Mods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods
RequiredMods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.RequiredMods,
AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods
});
}
@ -229,6 +231,14 @@ namespace osu.Game.Online.Multiplayer
public abstract Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability);
/// <summary>
/// Change the local user's mods in the currently joined room.
/// </summary>
/// <param name="newMods">The proposed new mods, excluding any required by the room itself.</param>
public Task ChangeUserMods(IEnumerable<Mod> newMods) => ChangeUserMods(newMods.Select(m => new APIMod(m)).ToList());
public abstract Task ChangeUserMods(IEnumerable<APIMod> newMods);
public abstract Task StartMatch();
Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state)
@ -377,6 +387,27 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask;
}
public Task UserModsChanged(int userId, IEnumerable<APIMod> mods)
{
if (Room == null)
return Task.CompletedTask;
Scheduler.Add(() =>
{
var user = Room?.Users.SingleOrDefault(u => u.UserID == userId);
// errors here are not critical - user mods are mostly for display.
if (user == null)
return;
user.Mods = mods;
RoomUpdated?.Invoke();
}, false);
return Task.CompletedTask;
}
Task IMultiplayerClient.LoadRequested()
{
if (Room == null)
@ -500,7 +531,8 @@ namespace osu.Game.Online.Multiplayer
beatmap.MD5Hash = settings.BeatmapChecksum;
var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance();
var mods = settings.Mods.Select(m => m.ToMod(ruleset));
var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset));
var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset));
PlaylistItem playlistItem = new PlaylistItem
{
@ -510,6 +542,7 @@ namespace osu.Game.Online.Multiplayer
};
playlistItem.RequiredMods.AddRange(mods);
playlistItem.AllowedMods.AddRange(allowedMods);
apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity.
apiRoom.Playlist.Add(playlistItem);

View File

@ -0,0 +1,63 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select;
using osuTK;
namespace osu.Game.Screens.OnlinePlay
{
public class FooterButtonFreeMods : FooterButton, IHasCurrentValue<IReadOnlyList<Mod>>
{
public Bindable<IReadOnlyList<Mod>> Current
{
get => modDisplay.Current;
set => modDisplay.Current = value;
}
private readonly ModDisplay modDisplay;
public FooterButtonFreeMods()
{
ButtonContentContainer.Add(modDisplay = new ModDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
DisplayUnrankedText = false,
Scale = new Vector2(0.8f),
ExpansionMode = ExpansionMode.AlwaysContracted,
});
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
SelectedColour = colours.Yellow;
DeselectedColour = SelectedColour.Opacity(0.5f);
Text = @"freemods";
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(_ => updateModDisplay(), true);
}
private void updateModDisplay()
{
if (Current.Value?.Count > 0)
modDisplay.FadeIn();
else
modDisplay.FadeOut();
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@ -29,6 +30,11 @@ namespace osu.Game.Screens.OnlinePlay.Match
[Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
/// <summary>
/// Any mods applied by/to the local user.
/// </summary>
protected readonly Bindable<IReadOnlyList<Mod>> UserMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
[Resolved]
private MusicController music { get; set; }
@ -68,6 +74,8 @@ namespace osu.Game.Screens.OnlinePlay.Match
managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(beatmapUpdated);
UserMods.BindValueChanged(_ => updateMods());
}
public override void OnEntering(IScreen last)
@ -86,6 +94,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
{
base.OnResuming(last);
beginHandlingTrack();
updateMods();
}
public override bool OnExiting(IScreen next)
@ -108,12 +117,17 @@ namespace osu.Game.Screens.OnlinePlay.Match
{
updateWorkingBeatmap();
var item = SelectedItem.Value;
if (SelectedItem.Value == null)
return;
Mods.Value = item?.RequiredMods?.ToArray() ?? Array.Empty<Mod>();
// Remove any user mods that are no longer allowed.
UserMods.Value = UserMods.Value
.Where(m => SelectedItem.Value.AllowedMods.Any(a => m.GetType() == a.GetType()))
.ToList();
if (item?.Ruleset != null)
Ruleset.Value = item.Ruleset.Value;
updateMods();
Ruleset.Value = SelectedItem.Value.Ruleset.Value;
}
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet) => Schedule(updateWorkingBeatmap);
@ -128,6 +142,14 @@ namespace osu.Game.Screens.OnlinePlay.Match
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
}
private void updateMods()
{
if (SelectedItem.Value == null)
return;
Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods).ToList();
}
private void beginHandlingTrack()
{
Beatmap.BindValueChanged(applyLoopingToTrack, true);

View File

@ -1,70 +1,32 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public class MultiplayerMatchSongSelect : SongSelect, IOnlinePlaySubScreen
public class MultiplayerMatchSongSelect : OnlinePlaySongSelect
{
public string ShortTitle => "song selection";
public override string Title => ShortTitle.Humanize();
[Resolved(typeof(Room), nameof(Room.Playlist))]
private BindableList<PlaylistItem> playlist { get; set; }
[Resolved]
private StatefulMultiplayerClient client { get; set; }
private LoadingLayer loadingLayer;
private WorkingBeatmap initialBeatmap;
private RulesetInfo initialRuleset;
private IReadOnlyList<Mod> initialMods;
private bool itemSelected;
public MultiplayerMatchSongSelect()
{
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(loadingLayer = new LoadingLayer(true));
initialBeatmap = Beatmap.Value;
initialRuleset = Ruleset.Value;
initialMods = Mods.Value.ToList();
}
protected override bool OnStart()
protected override void SelectItem(PlaylistItem item)
{
itemSelected = true;
var item = new PlaylistItem();
item.Beatmap.Value = Beatmap.Value.BeatmapInfo;
item.Ruleset.Value = Ruleset.Value;
item.RequiredMods.Clear();
item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy()));
// If the client is already in a room, update via the client.
// Otherwise, update the playlist directly in preparation for it to be submitted to the API on match creation.
if (client.Room != null)
@ -89,30 +51,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
else
{
playlist.Clear();
playlist.Add(item);
Playlist.Clear();
Playlist.Add(item);
this.Exit();
}
return true;
}
public override bool OnExiting(IScreen next)
{
if (!itemSelected)
{
Beatmap.Value = initialBeatmap;
Ruleset.Value = initialRuleset;
Mods.Value = initialMods;
}
return base.OnExiting(next);
}
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay { IsValidMod = isValidMod };
private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true;
protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust) && !mod.RequiresConfiguration;
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
@ -13,12 +14,16 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
using osuTK;
using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.ParticipantsList;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
@ -36,7 +41,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
private ModSelectOverlay userModsSelectOverlay;
private MultiplayerMatchSettingsOverlay settingsOverlay;
private Drawable userModsSection;
private IBindable<bool> isConnected;
@ -90,18 +97,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
},
new Drawable[]
{
new GridContainer
new Container
{
RelativeSizeAxes = Axes.Both,
Content = new[]
Padding = new MarginPadding { Horizontal = 5, Vertical = 10 },
Child = new GridContainer
{
new Drawable[]
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Container
new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 400),
new Dimension(),
new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 600),
},
Content = new[]
{
new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 5, Vertical = 10 },
Child = new GridContainer
// Main left column
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
@ -119,19 +133,62 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
},
}
}
}
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 5 },
Children = new Drawable[]
},
// Spacer
null,
// Main right column
new FillFlowContainer
{
new OverlinedHeader("Beatmap"),
new BeatmapSelectionControl { RelativeSizeAxes = Axes.X }
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new OverlinedHeader("Beatmap"),
new BeatmapSelectionControl { RelativeSizeAxes = Axes.X }
}
},
userModsSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 10 },
Children = new Drawable[]
{
new OverlinedHeader("Extra mods"),
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new PurpleTriangleButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = () => userModsSelectOverlay.Show()
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
DisplayUnrankedText = false,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
}
}
}
}
}
}
}
@ -173,6 +230,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
new Dimension(GridSizeMode.AutoSize),
}
},
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Child = userModsSelectOverlay = new UserModSelectOverlay
{
SelectedMods = { BindTarget = UserMods },
IsValidMod = _ => false
}
},
settingsOverlay = new MultiplayerMatchSettingsOverlay
{
RelativeSizeAxes = Axes.Both,
@ -199,6 +268,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
base.LoadComplete();
Playlist.BindCollectionChanged(onPlaylistChanged, true);
UserMods.BindValueChanged(onUserModsChanged);
client.LoadRequested += onLoadRequested;
@ -218,10 +288,39 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return true;
}
if (userModsSelectOverlay.State.Value == Visibility.Visible)
{
userModsSelectOverlay.Hide();
return true;
}
return base.OnBackButton();
}
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault();
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e)
{
SelectedItem.Value = Playlist.FirstOrDefault();
if (SelectedItem.Value?.AllowedMods.Any() != true)
{
userModsSection.Hide();
userModsSelectOverlay.Hide();
userModsSelectOverlay.IsValidMod = _ => false;
}
else
{
userModsSection.Show();
userModsSelectOverlay.IsValidMod = m => SelectedItem.Value.AllowedMods.Any(a => a.GetType() == m.GetType());
}
}
private void onUserModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
if (client.Room == null)
return;
client.ChangeUserMods(mods.NewValue);
}
private void onReadyClick()
{
@ -274,5 +373,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client != null)
client.LoadRequested -= onLoadRequested;
}
private class UserModSelectOverlay : ModSelectOverlay
{
public UserModSelectOverlay()
{
CustomiseButton.Alpha = 0;
}
}
}
}

View File

@ -1,6 +1,7 @@
// 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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@ -15,6 +16,8 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK;
@ -29,6 +32,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; }
private ModDisplay userModsDisplay;
private StateDisplay userStateDisplay;
private SpriteIcon crown;
@ -121,6 +128,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
}
}
},
new Container
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Right = 70 },
Child = userModsDisplay = new ModDisplay
{
Scale = new Vector2(0.5f),
ExpansionMode = ExpansionMode.AlwaysContracted,
DisplayUnrankedText = false,
}
},
userStateDisplay = new StateDisplay
{
Anchor = Anchor.CentreRight,
@ -142,7 +162,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
const double fade_time = 50;
var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID).CreateInstance();
userStateDisplay.Status = User.State;
userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(ruleset)).ToList();
if (Room.Host?.Equals(User) == true)
crown.FadeIn(fade_time);

View File

@ -0,0 +1,154 @@
// 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 System.Collections.Generic;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osu.Game.Utils;
namespace osu.Game.Screens.OnlinePlay
{
public abstract class OnlinePlaySongSelect : SongSelect, IOnlinePlaySubScreen
{
public string ShortTitle => "song selection";
public override string Title => ShortTitle.Humanize();
public override bool AllowEditing => false;
[Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
private readonly Bindable<IReadOnlyList<Mod>> freeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
private readonly FreeModSelectOverlay freeModSelectOverlay;
private WorkingBeatmap initialBeatmap;
private RulesetInfo initialRuleset;
private IReadOnlyList<Mod> initialMods;
private bool itemSelected;
protected OnlinePlaySongSelect()
{
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
freeModSelectOverlay = new FreeModSelectOverlay
{
SelectedMods = { BindTarget = freeMods },
IsValidMod = IsValidFreeMod,
};
}
[BackgroundDependencyLoader]
private void load()
{
initialBeatmap = Beatmap.Value;
initialRuleset = Ruleset.Value;
initialMods = Mods.Value.ToList();
FooterPanels.Add(freeModSelectOverlay);
}
protected override void LoadComplete()
{
base.LoadComplete();
// At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods.
// Similarly, freeMods is currently empty but should only contain the allowed mods.
Mods.Value = Playlist.FirstOrDefault()?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
freeMods.Value = Playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
Ruleset.BindValueChanged(onRulesetChanged);
}
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset)
{
freeMods.Value = Array.Empty<Mod>();
}
protected sealed override bool OnStart()
{
itemSelected = true;
var item = new PlaylistItem();
item.Beatmap.Value = Beatmap.Value.BeatmapInfo;
item.Ruleset.Value = Ruleset.Value;
item.RequiredMods.Clear();
item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy()));
item.AllowedMods.Clear();
item.AllowedMods.AddRange(freeMods.Value.Select(m => m.CreateCopy()));
SelectItem(item);
return true;
}
/// <summary>
/// Invoked when the user has requested a selection of a beatmap.
/// </summary>
/// <param name="item">The resultant <see cref="PlaylistItem"/>. This item has not yet been added to the <see cref="Room"/>'s.</param>
protected abstract void SelectItem(PlaylistItem item);
public override bool OnBackButton()
{
if (freeModSelectOverlay.State.Value == Visibility.Visible)
{
freeModSelectOverlay.Hide();
return true;
}
return base.OnBackButton();
}
public override bool OnExiting(IScreen next)
{
if (!itemSelected)
{
Beatmap.Value = initialBeatmap;
Ruleset.Value = initialRuleset;
Mods.Value = initialMods;
}
return base.OnExiting(next);
}
protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay
{
IsValidMod = IsValidMod
};
protected override IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons()
{
var buttons = base.CreateFooterButtons().ToList();
buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = freeMods }, freeModSelectOverlay));
return buttons;
}
/// <summary>
/// Checks whether a given <see cref="Mod"/> is valid for global selection.
/// </summary>
/// <param name="mod">The <see cref="Mod"/> to check.</param>
/// <returns>Whether <paramref name="mod"/> is a valid mod for online play.</returns>
protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && !ModUtils.FlattenMod(mod).Any(m => m is ModAutoplay);
/// <summary>
/// Checks whether a given <see cref="Mod"/> is valid for per-player free-mod selection.
/// </summary>
/// <param name="mod">The <see cref="Mod"/> to check.</param>
/// <returns>Whether <paramref name="mod"/> is a selectable free-mod.</returns>
protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod);
}
}

View File

@ -13,7 +13,6 @@ using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.Select;
using osu.Game.Users;
using Footer = osu.Game.Screens.OnlinePlay.Match.Components.Footer;
@ -188,7 +187,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
settingsOverlay = new PlaylistsMatchSettingsOverlay
{
RelativeSizeAxes = Axes.Both,
EditPlaylist = () => this.Push(new MatchSongSelect()),
EditPlaylist = () => this.Push(new PlaylistsSongSelect()),
State = { Value = roomId.Value == null ? Visibility.Visible : Visibility.Hidden }
}
};

View File

@ -1,48 +1,27 @@
// 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 System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.Select;
namespace osu.Game.Screens.Select
namespace osu.Game.Screens.OnlinePlay.Playlists
{
public class MatchSongSelect : SongSelect, IOnlinePlaySubScreen
public class PlaylistsSongSelect : OnlinePlaySongSelect
{
public Action<PlaylistItem> Selected;
public string ShortTitle => "song selection";
public override string Title => ShortTitle.Humanize();
public override bool AllowEditing => false;
[Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
public MatchSongSelect()
{
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
}
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new MatchBeatmapDetailArea
{
CreateNewItem = createNewItem
};
protected override bool OnStart()
protected override void SelectItem(PlaylistItem item)
{
switch (Playlist.Count)
{
@ -56,8 +35,6 @@ namespace osu.Game.Screens.Select
}
this.Exit();
return true;
}
private void createNewItem()
@ -80,9 +57,5 @@ namespace osu.Game.Screens.Select
item.RequiredMods.Clear();
item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy()));
}
protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay { IsValidMod = isValidMod };
private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true;
}
}

View File

@ -28,19 +28,16 @@ namespace osu.Game.Screens.Select
private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
/// <param name="button">THe button to be added.</param>
/// <param name="button">The button to be added.</param>
/// <param name="overlay">The <see cref="OverlayContainer"/> to be toggled by this button.</param>
public void AddButton(FooterButton button, OverlayContainer overlay)
{
overlays.Add(overlay);
button.Action = () => showOverlay(overlay);
if (overlay != null)
{
overlays.Add(overlay);
button.Action = () => showOverlay(overlay);
}
AddButton(button);
}
/// <param name="button">Button to be added.</param>
public void AddButton(FooterButton button)
{
button.Hovered = updateModeLight;
button.HoverLost = updateModeLight;

View File

@ -263,9 +263,8 @@ namespace osu.Game.Screens.Select
if (Footer != null)
{
Footer.AddButton(new FooterButtonMods { Current = Mods }, ModSelect);
Footer.AddButton(new FooterButtonRandom { Action = triggerRandom });
Footer.AddButton(new FooterButtonOptions(), BeatmapOptions);
foreach (var (button, overlay) in CreateFooterButtons())
Footer.AddButton(button, overlay);
BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show());
BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo));
@ -301,6 +300,17 @@ namespace osu.Game.Screens.Select
}
}
/// <summary>
/// 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)> CreateFooterButtons() => new (FooterButton, OverlayContainer)[]
{
(new FooterButtonMods { Current = Mods }, ModSelect),
(new FooterButtonRandom { Action = triggerRandom }, null),
(new FooterButtonOptions(), BeatmapOptions)
};
protected virtual ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay();
protected virtual void ApplyFilterToCarousel(FilterCriteria criteria)

View File

@ -3,6 +3,7 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
@ -11,6 +12,7 @@ using osu.Framework.Bindables;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
@ -122,6 +124,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
return Task.CompletedTask;
}
public void ChangeUserMods(int userId, IEnumerable<Mod> newMods)
=> ChangeUserMods(userId, newMods.Select(m => new APIMod(m)).ToList());
public void ChangeUserMods(int userId, IEnumerable<APIMod> newMods)
{
Debug.Assert(Room != null);
((IMultiplayerClient)this).UserModsChanged(userId, newMods.ToList());
}
public override Task ChangeUserMods(IEnumerable<APIMod> newMods)
{
ChangeUserMods(api.LocalUser.Value.Id, newMods);
return Task.CompletedTask;
}
public override Task StartMatch()
{
Debug.Assert(Room != null);