1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-18 11:52:54 +08:00

Rewrite everything to better support spectator server messaging

This commit is contained in:
Dan Balasescu 2024-12-25 22:24:21 +09:00
parent 95fe8d67e4
commit 0093af8f55
No known key found for this signature in database
12 changed files with 417 additions and 202 deletions

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

@ -359,6 +359,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>
@ -652,6 +654,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>
/// Any ruleset applicable only to the local user.
/// </summary>
[Key(5)]
public int? RulesetId;
/// <summary>
/// Any beatmap applicable only to the local user.
/// </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

@ -4,14 +4,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@ -28,6 +26,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
@ -37,18 +36,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
{
public readonly Bindable<PlaylistItem?> SelectedItem = new Bindable<PlaylistItem?>();
/// <summary>
/// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id),
/// a non-null value indicates a local beatmap selection from the same beatmapset as the selected item.
/// </summary>
public readonly Bindable<BeatmapInfo?> DifficultyOverride = new Bindable<BeatmapInfo?>();
/// <summary>
/// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id),
/// a non-null value indicates a local ruleset selection.
/// </summary>
public readonly Bindable<RulesetInfo?> RulesetOverride = new Bindable<RulesetInfo?>();
public override bool? ApplyModTrackAdjustments => true;
protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(Room.Playlist.FirstOrDefault())
@ -65,13 +52,13 @@ namespace osu.Game.Screens.OnlinePlay.Match
protected Drawable? UserModsSection;
/// <summary>
/// A container that provides controls for selection of the user's difficulty override.
/// A container that provides controls for selection of the user style.
/// This will be shown/hidden automatically when applicable.
/// </summary>
protected Drawable? UserDifficultySection;
protected Drawable? UserStyleSection;
/// <summary>
/// A container that will display the user's difficulty override.
/// A container that will display the user's style.
/// </summary>
protected Container<DrawableRoomPlaylistItem>? UserStyleDisplayContainer;
@ -82,6 +69,18 @@ namespace osu.Game.Screens.OnlinePlay.Match
/// </summary>
protected readonly Bindable<IReadOnlyList<Mod>> UserMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
/// <summary>
/// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id),
/// a non-null value indicates a local beatmap selection from the same beatmapset as the selected item.
/// </summary>
public readonly Bindable<BeatmapInfo?> UserBeatmap = new Bindable<BeatmapInfo?>();
/// <summary>
/// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id),
/// a non-null value indicates a local ruleset selection.
/// </summary>
public readonly Bindable<RulesetInfo?> UserRuleset = new Bindable<RulesetInfo?>();
[Resolved(CanBeNull = true)]
private IOverlayManager? overlayManager { get; set; }
@ -272,13 +271,25 @@ namespace osu.Game.Screens.OnlinePlay.Match
{
base.LoadComplete();
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged));
UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods));
DifficultyOverride.BindValueChanged(_ => Scheduler.AddOnce(updateStyleOverride));
RulesetOverride.BindValueChanged(_ => Scheduler.AddOnce(updateStyleOverride));
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged));
UserMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods));
UserBeatmap.BindValueChanged(_ => Scheduler.AddOnce(() =>
{
updateBeatmap();
updateUserStyle();
}));
UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(() =>
{
updateUserMods();
updateRuleset();
updateUserStyle();
}));
beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem);
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateWorkingBeatmap());
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateBeatmap());
userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(UserModsSelectOverlay);
@ -347,7 +358,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();
updateBeatmap();
onLeaving();
base.OnSuspending(e);
@ -356,10 +367,11 @@ namespace osu.Game.Screens.OnlinePlay.Match
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(e);
updateWorkingBeatmap();
updateBeatmap();
beginHandlingTrack();
Scheduler.AddOnce(UpdateMods);
Scheduler.AddOnce(updateMods);
Scheduler.AddOnce(updateRuleset);
Scheduler.AddOnce(updateUserStyle);
}
protected bool ExitConfirmed { get; private set; }
@ -409,9 +421,13 @@ namespace osu.Game.Screens.OnlinePlay.Match
protected void StartPlay()
{
if (GetGameplayItem() is not PlaylistItem item)
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())
@ -437,31 +453,26 @@ namespace osu.Game.Screens.OnlinePlay.Match
/// <returns>The screen to enter.</returns>
protected abstract Screen CreateGameplayScreen(PlaylistItem selectedItem);
private void selectedItemChanged()
protected void OnSelectedItemChanged()
{
if (SelectedItem.Value is not PlaylistItem selected)
if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item)
return;
if (selected.BeatmapSetId == null || selected.BeatmapSetId != DifficultyOverride.Value?.BeatmapSet.AsNonNull().OnlineID)
// Reset user style if no longer valid.
// Todo: In the future this can be made more lenient, such as allowing a non-null ruleset as the set changes.
if (item.BeatmapSetId == null || item.BeatmapSetId != UserBeatmap.Value?.BeatmapSet!.OnlineID)
{
DifficultyOverride.Value = null;
RulesetOverride.Value = null;
UserBeatmap.Value = null;
UserRuleset.Value = null;
}
updateStyleOverride();
updateWorkingBeatmap();
var rulesetInstance = Rulesets.GetRuleset(selected.RulesetID)?.CreateInstance();
Debug.Assert(rulesetInstance != null);
var allowedMods = selected.AllowedMods.Select(m => m.ToMod(rulesetInstance));
// Remove any user mods that are no longer allowed.
UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList();
UpdateMods();
updateUserMods();
updateBeatmap();
updateMods();
updateRuleset();
updateUserStyle();
if (!selected.AllowedMods.Any())
if (!item.AllowedMods.Any())
{
UserModsSection?.Hide();
UserModsSelectOverlay.Hide();
@ -470,100 +481,89 @@ namespace osu.Game.Screens.OnlinePlay.Match
else
{
UserModsSection?.Show();
var rulesetInstance = GetGameplayRuleset().CreateInstance();
var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance));
UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
}
if (selected.BeatmapSetId == null)
UserDifficultySection?.Hide();
if (item.BeatmapSetId == null)
UserStyleSection?.Hide();
else
UserDifficultySection?.Show();
UserStyleSection?.Show();
}
private void updateWorkingBeatmap()
private void updateUserMods()
{
if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen())
if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item)
return;
// Remove any user mods that are no longer allowed.
var rulesetInstance = GetGameplayRuleset().CreateInstance();
var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance));
UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList();
}
private void updateBeatmap()
{
if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem)
return;
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID);
UserModsSelectOverlay.Beatmap.Value = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
int beatmapId = GetGameplayBeatmap().OnlineID;
var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId);
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
UserModsSelectOverlay.Beatmap.Value = Beatmap.Value;
}
protected virtual void UpdateMods()
private void updateMods()
{
if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen())
if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem)
return;
var rulesetInstance = Rulesets.GetRuleset(item.RulesetID)?.CreateInstance();
Debug.Assert(rulesetInstance != null);
Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList();
}
private void updateStyleOverride()
{
if (SelectedItem.Value == null || !this.IsCurrentScreen())
return;
if (UserStyleDisplayContainer == null)
return;
PlaylistItem gameplayItem = GetGameplayItem()!;
if (UserStyleDisplayContainer.SingleOrDefault()?.Item.Equals(gameplayItem) == true)
return;
UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem)
{
AllowReordering = false,
AllowEditing = true,
RequestEdit = _ => openStyleSelection()
};
}
protected PlaylistItem? GetGameplayItem()
{
PlaylistItem? selectedItemWithOverride = SelectedItem.Value;
if (selectedItemWithOverride?.BeatmapSetId == null)
return selectedItemWithOverride;
// Sanity check.
if (DifficultyOverride.Value?.BeatmapSet?.OnlineID != selectedItemWithOverride.BeatmapSetId)
return selectedItemWithOverride;
if (DifficultyOverride.Value != null)
selectedItemWithOverride = selectedItemWithOverride.With(beatmap: DifficultyOverride.Value);
if (RulesetOverride.Value != null)
selectedItemWithOverride = selectedItemWithOverride.With(ruleset: RulesetOverride.Value.OnlineID);
return selectedItemWithOverride;
}
private void openStyleSelection()
{
if (SelectedItem.Value == null || !this.IsCurrentScreen())
return;
this.Push(new MultiplayerMatchStyleSelect(Room, SelectedItem.Value, (beatmap, ruleset) =>
{
if (SelectedItem.Value?.BeatmapSetId == null || SelectedItem.Value.BeatmapSetId != beatmap.BeatmapSet?.OnlineID)
return;
DifficultyOverride.Value = beatmap;
RulesetOverride.Value = ruleset;
}));
var rulesetInstance = GetGameplayRuleset().CreateInstance();
Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray();
}
private void updateRuleset()
{
if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen())
if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem)
return;
Ruleset.Value = Rulesets.GetRuleset(item.RulesetID);
Ruleset.Value = GetGameplayRuleset();
}
private void updateUserStyle()
{
if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem)
return;
if (UserStyleDisplayContainer != null)
{
PlaylistItem gameplayItem = SelectedItem.Value.With(
ruleset: GetGameplayRuleset().OnlineID,
beatmap: new Optional<IBeatmapInfo>(GetGameplayBeatmap()));
UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem)
{
AllowReordering = false,
AllowEditing = true,
RequestEdit = _ => OpenStyleSelection()
};
}
}
protected virtual APIMod[] GetGameplayMods()
=> UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray();
protected virtual RulesetInfo GetGameplayRuleset()
=> Rulesets.GetRuleset(UserRuleset.Value?.OnlineID ?? SelectedItem.Value!.RulesetID)!;
protected virtual IBeatmapInfo GetGameplayBeatmap()
=> UserBeatmap.Value ?? SelectedItem.Value!.Beatmap;
protected abstract void OpenStyleSelection();
private void beginHandlingTrack()
{
Beatmap.BindValueChanged(applyLoopingToTrack, true);

View File

@ -2,106 +2,88 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.Select;
using osu.Game.Users;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public partial class MultiplayerMatchStyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap
public partial class MultiplayerMatchStyleSelect : OnlinePlayStyleSelect
{
public string ShortTitle => "style selection";
[Resolved]
private MultiplayerClient client { get; set; } = null!;
public override string Title => ShortTitle.Humanize();
[Resolved]
private OngoingOperationTracker operationTracker { get; set; } = null!;
public override bool AllowEditing => false;
private readonly IBindable<bool> operationInProgress = new Bindable<bool>();
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
private LoadingLayer loadingLayer = null!;
private IDisposable? selectionOperation;
private readonly Room room;
private readonly PlaylistItem item;
private readonly Action<BeatmapInfo, RulesetInfo> onSelect;
public MultiplayerMatchStyleSelect(Room room, PlaylistItem item, Action<BeatmapInfo, RulesetInfo> onSelect)
public MultiplayerMatchStyleSelect(Room room, PlaylistItem item)
: base(room, item)
{
this.room = room;
this.item = item;
this.onSelect = onSelect;
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
}
[BackgroundDependencyLoader]
private void load()
{
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
AddInternal(loadingLayer = new LoadingLayer(true));
}
protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item);
protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons()
protected override void LoadComplete()
{
// Required to create the drawable components.
base.CreateSongSelectFooterButtons();
return Enumerable.Empty<(FooterButton, OverlayContainer?)>();
base.LoadComplete();
operationInProgress.BindTo(operationTracker.InProgress);
operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true);
}
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
private void updateLoadingLayer()
{
if (operationInProgress.Value)
loadingLayer.Show();
else
loadingLayer.Hide();
}
protected override bool OnStart()
{
onSelect(Beatmap.Value.BeatmapInfo, Ruleset.Value);
this.Exit();
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;
}
public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
{
// This screen cannot present beatmaps.
}
private partial class DifficultySelectFilterControl : FilterControl
{
private readonly PlaylistItem item;
private double itemLength;
public DifficultySelectFilterControl(PlaylistItem item)
{
this.item = item;
}
[BackgroundDependencyLoader]
private void load(RealmAccess realm)
{
int beatmapId = item.Beatmap.OnlineID;
itemLength = realm.Run(r => r.All<BeatmapInfo>().FirstOrDefault(b => b.OnlineID == beatmapId)?.Length ?? 0);
}
public override FilterCriteria CreateCriteria()
{
var criteria = base.CreateCriteria();
// Must be from the same set as the playlist item.
criteria.BeatmapSetId = item.BeatmapSetId;
// 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

@ -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;
@ -188,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
},
}
},
UserDifficultySection = new FillFlowContainer
UserStyleSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@ -251,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 MultiplayerMatchStyleSelect(Room, item));
}
protected override Drawable CreateFooter() => new MultiplayerMatchFooter
{
SelectedItem = SelectedItem
@ -261,16 +271,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
SelectedItem = SelectedItem
};
protected override void UpdateMods()
protected override APIMod[] GetGameplayMods()
{
if (GetGameplayItem() is not PlaylistItem item || client.LocalUser == null || !this.IsCurrentScreen())
return;
// Using the room's reported status makes the server authoritative.
return client.LocalUser?.Mods.Concat(SelectedItem.Value!.RequiredMods).ToArray()!;
}
// 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(item.RulesetID)?.CreateInstance();
Debug.Assert(rulesetInstance != null);
Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(rulesetInstance)).Concat(item.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)]
@ -376,7 +392,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
addItemButton.Alpha = localUserCanAddItem ? 1 : 0;
Scheduler.AddOnce(UpdateMods);
// Forcefully update the selected item so that the user state is applied.
Scheduler.AddOnce(OnSelectedItemChanged);
Activity.Value = new UserActivity.InLobby(Room);
}

View File

@ -0,0 +1,98 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
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 OnlinePlayStyleSelect : 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 OnlinePlayStyleSelect(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, OverlayContainer?)> 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;
public DifficultySelectFilterControl(PlaylistItem item)
{
this.item = item;
}
[BackgroundDependencyLoader]
private void load(RealmAccess realm)
{
int beatmapId = item.Beatmap.OnlineID;
itemLength = realm.Run(r => r.All<BeatmapInfo>().FirstOrDefault(b => b.OnlineID == beatmapId)?.Length ?? 0);
}
public override FilterCriteria CreateCriteria()
{
var criteria = base.CreateCriteria();
// Must be from the same set as the playlist item.
criteria.BeatmapSetId = item.BeatmapSetId;
// 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

@ -0,0 +1,30 @@
// 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 PlaylistsRoomStyleSelect : OnlinePlayStyleSelect
{
public new readonly Bindable<BeatmapInfo?> Beatmap = new Bindable<BeatmapInfo?>();
public new readonly Bindable<RulesetInfo?> Ruleset = new Bindable<RulesetInfo?>();
public PlaylistsRoomStyleSelect(Room room, PlaylistItem item)
: base(room, item)
{
}
protected override bool OnStart()
{
Beatmap.Value = base.Beatmap.Value.BeatmapInfo;
Ruleset.Value = base.Ruleset.Value;
this.Exit();
return true;
}
}
}

View File

@ -213,7 +213,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}
}
},
UserDifficultySection = new FillFlowContainer
UserStyleSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@ -299,6 +299,18 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
},
};
protected override void OpenStyleSelection()
{
if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item)
return;
this.Push(new PlaylistsRoomStyleSelect(Room, item)
{
Beatmap = { BindTarget = UserBeatmap },
Ruleset = { BindTarget = UserRuleset }
});
}
private void updatePollingRate()
{
selectionPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000;

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)));