1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-23 01:27:35 +08:00
osu-lazer/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs

517 lines
20 KiB
C#
Raw Normal View History

// 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.
2020-12-29 15:20:43 +08:00
using System;
2021-02-01 16:54:56 +08:00
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
2020-12-29 15:20:43 +08:00
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
2021-02-10 18:56:59 +08:00
using osu.Framework.Threading;
using osu.Game.Beatmaps;
2021-02-10 18:56:59 +08:00
using osu.Game.Configuration;
using osu.Game.Online;
using osu.Game.Online.Multiplayer;
2020-12-25 12:38:11 +08:00
using osu.Game.Online.Rooms;
2021-03-03 13:50:54 +08:00
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
2021-02-01 16:54:56 +08:00
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;
2021-12-01 18:54:48 +08:00
using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
2021-04-09 20:03:45 +08:00
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
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
{
[Cached]
public class MultiplayerMatchSubScreen : RoomSubScreen, IHandlePresentBeatmap
{
public override string Title { get; }
public override string ShortTitle => "room";
[Resolved]
2021-05-20 14:39:45 +08:00
private MultiplayerClient client { get; set; }
2020-12-29 15:20:43 +08:00
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
2020-12-29 03:59:38 +08:00
private readonly IBindable<bool> isConnected = new Bindable<bool>();
2020-12-29 15:20:43 +08:00
[CanBeNull]
private IDisposable readyClickOperation;
2020-12-29 15:20:43 +08:00
private AddItemButton addItemButton;
2020-12-25 12:38:11 +08:00
public MultiplayerMatchSubScreen(Room room)
2021-08-17 16:05:20 +08:00
: base(room)
{
Title = room.RoomID.Value == null ? "New room" : room.Name.Value;
Activity.Value = new UserActivity.InLobby(room);
}
protected override void LoadComplete()
{
base.LoadComplete();
BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true);
UserMods.BindValueChanged(onUserModsChanged);
client.LoadRequested += onLoadRequested;
client.RoomUpdated += onRoomUpdated;
isConnected.BindTo(client.IsConnected);
isConnected.BindValueChanged(connected =>
{
if (!connected.NewValue)
handleRoomLost();
}, true);
}
2021-10-15 19:18:25 +08:00
protected override Drawable CreateMainContent() => new Container
2021-08-17 16:05:20 +08:00
{
RelativeSizeAxes = Axes.Both,
2021-10-15 19:18:25 +08:00
Padding = new MarginPadding { Horizontal = 5, Vertical = 10 },
Child = new GridContainer
2021-08-17 16:05:20 +08:00
{
2021-10-15 19:18:25 +08:00
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
2021-08-17 16:05:20 +08:00
{
2021-10-15 19:18:25 +08:00
new Dimension(),
2021-10-15 19:25:32 +08:00
new Dimension(GridSizeMode.Absolute, 10),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 10),
new Dimension(),
2021-10-15 19:18:25 +08:00
},
Content = new[]
{
new Drawable[]
2021-08-17 16:05:20 +08:00
{
2021-10-15 19:25:32 +08:00
// Participants column
2021-10-15 19:18:25 +08:00
new GridContainer
2021-08-17 16:05:20 +08:00
{
2021-08-18 14:54:33 +08:00
RelativeSizeAxes = Axes.Both,
2021-10-15 19:18:25 +08:00
RowDimensions = new[]
2021-08-17 16:05:20 +08:00
{
2021-10-15 19:18:25 +08:00
new Dimension(GridSizeMode.AutoSize)
2021-08-18 14:54:33 +08:00
},
Content = new[]
{
2021-10-15 19:18:25 +08:00
new Drawable[] { new ParticipantsListHeader() },
2021-08-18 14:54:33 +08:00
new Drawable[]
2021-08-17 16:05:20 +08:00
{
2021-10-15 19:18:25 +08:00
new ParticipantsList
2021-08-17 16:05:20 +08:00
{
2021-10-15 19:18:25 +08:00
RelativeSizeAxes = Axes.Both
2021-08-18 14:54:33 +08:00
},
2021-10-15 19:18:25 +08:00
}
}
},
// Spacer
null,
2021-10-15 19:25:32 +08:00
// Beatmap column
2021-10-15 19:18:25 +08:00
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[] { new OverlinedHeader("Beatmap") },
new Drawable[]
{
addItemButton = new AddItemButton
{
RelativeSizeAxes = Axes.X,
Height = 40,
Text = "Add item",
Action = () => OpenSongSelection()
},
},
null,
new Drawable[]
{
2021-12-01 18:54:48 +08:00
new MultiplayerPlaylist
{
RelativeSizeAxes = Axes.Both,
RequestEdit = item => OpenSongSelection(item.ID)
2021-12-01 18:54:48 +08:00
}
},
2021-10-15 19:18:25 +08:00
new[]
{
UserModsSection = new FillFlowContainer
2021-08-18 14:54:33 +08:00
{
2021-10-15 19:18:25 +08:00
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 10 },
Alpha = 0,
Children = new Drawable[]
2021-08-18 14:54:33 +08:00
{
2021-10-15 19:18:25 +08:00
new OverlinedHeader("Extra mods"),
new FillFlowContainer
2021-08-18 14:54:33 +08:00
{
2021-10-15 19:18:25 +08:00
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
2021-08-18 14:54:33 +08:00
{
2021-10-15 19:18:25 +08:00
new UserModSelectButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
2021-08-17 16:05:20 +08:00
{
2021-10-15 19:18:25 +08:00
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
},
2021-08-17 16:05:20 +08:00
}
},
2021-10-15 19:18:25 +08:00
},
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 5),
new Dimension(),
2021-10-15 19:18:25 +08:00
new Dimension(GridSizeMode.AutoSize),
2021-10-15 19:25:32 +08:00
}
},
// Spacer
null,
// Main right column
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[] { new OverlinedHeader("Chat") },
new Drawable[] { new MatchChatDisplay(Room) { RelativeSizeAxes = Axes.Both } }
},
RowDimensions = new[]
{
2021-10-15 19:18:25 +08:00
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
2021-08-17 16:05:20 +08:00
}
2021-10-15 19:18:25 +08:00
},
2021-08-17 16:05:20 +08:00
}
2021-10-15 19:18:25 +08:00
}
}
2021-08-17 16:05:20 +08:00
};
/// <summary>
/// Opens the song selection screen to add or edit an item.
/// </summary>
/// <param name="itemToEdit">An optional playlist item to edit. If null, a new item will be added instead.</param>
internal void OpenSongSelection(long? itemToEdit = null)
{
if (!this.IsCurrentScreen())
return;
this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit));
}
2021-08-17 16:05:20 +08:00
protected override Drawable CreateFooter() => new MultiplayerMatchFooter
{
OnReadyClick = onReadyClick,
OnSpectateClick = onSpectateClick
};
2021-08-19 15:40:27 +08:00
protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room);
2021-08-18 19:21:29 +08:00
protected override void UpdateMods()
{
if (SelectedItem.Value == null || client.LocalUser == null)
return;
// 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 ruleset = Ruleset.Value.CreateInstance();
Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList();
}
[Resolved(canBeNull: true)]
2021-03-03 13:50:54 +08:00
private DialogOverlay dialogOverlay { get; set; }
private bool exitConfirmed;
public override bool OnExiting(IScreen next)
{
// the room may not be left immediately after a disconnection due to async flow,
// so checking the IsConnected status is also required.
if (client.Room == null || !client.IsConnected.Value)
{
// room has not been created yet; exit immediately.
return base.OnExiting(next);
}
if (!exitConfirmed && dialogOverlay != null)
2021-03-03 13:50:54 +08:00
{
if (dialogOverlay.CurrentDialog is ConfirmDialog confirmDialog)
confirmDialog.PerformOkAction();
else
2021-03-03 13:50:54 +08:00
{
dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
{
exitConfirmed = true;
this.Exit();
}));
}
2021-03-03 13:50:54 +08:00
return true;
}
return base.OnExiting(next);
}
2021-02-10 18:56:59 +08:00
private ModSettingChangeTracker modSettingChangeTracker;
private ScheduledDelegate debouncedModSettingsUpdate;
2021-02-01 16:57:32 +08:00
private void onUserModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
2021-02-01 16:54:56 +08:00
{
2021-02-10 18:56:59 +08:00
modSettingChangeTracker?.Dispose();
2021-02-01 16:54:56 +08:00
if (client.Room == null)
return;
2021-02-01 18:28:33 +08:00
client.ChangeUserMods(mods.NewValue);
2021-02-10 18:56:59 +08:00
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
modSettingChangeTracker.SettingChanged += onModSettingsChanged;
}
private void onModSettingsChanged(Mod mod)
{
// Debounce changes to mod settings so as to not thrash the network.
debouncedModSettingsUpdate?.Cancel();
2021-02-10 19:16:26 +08:00
debouncedModSettingsUpdate = Scheduler.AddDelayed(() =>
{
if (client.Room == null)
return;
client.ChangeUserMods(UserMods.Value);
}, 500);
2021-02-01 16:54:56 +08:00
}
private void updateBeatmapAvailability(ValueChangedEvent<BeatmapAvailability> availability)
{
if (client.Room == null)
return;
client.ChangeBeatmapAvailability(availability.NewValue);
2021-02-05 15:19:45 +08:00
if (availability.NewValue.State != DownloadState.LocallyAvailable)
{
// while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap.
if (client.LocalUser?.State == MultiplayerUserState.Ready)
client.ChangeState(MultiplayerUserState.Idle);
}
else
{
if (client.LocalUser?.State == MultiplayerUserState.Spectating && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing))
onLoadRequested();
}
}
private void onReadyClick()
{
Debug.Assert(readyClickOperation == null);
readyClickOperation = ongoingOperationTracker.BeginOperation();
2021-04-07 19:46:30 +08:00
if (client.IsHost && (client.LocalUser?.State == MultiplayerUserState.Ready || client.LocalUser?.State == MultiplayerUserState.Spectating))
{
client.StartMatch()
.ContinueWith(t =>
{
// accessing Exception here silences any potential errors from the antecedent task
if (t.Exception != null)
{
// gameplay was not started due to an exception; unblock button.
endOperation();
}
// gameplay is starting, the button will be unblocked on load requested.
});
return;
}
client.ToggleReady()
.ContinueWith(t => endOperation());
void endOperation()
{
readyClickOperation?.Dispose();
readyClickOperation = null;
}
}
2021-04-07 19:46:30 +08:00
private void onSpectateClick()
{
Debug.Assert(readyClickOperation == null);
readyClickOperation = ongoingOperationTracker.BeginOperation();
client.ToggleSpectate().ContinueWith(t => endOperation());
void endOperation()
{
readyClickOperation?.Dispose();
readyClickOperation = null;
}
}
private void onRoomUpdated()
{
2021-08-11 18:56:25 +08:00
// may happen if the client is kicked or otherwise removed from the room.
if (client.Room == null)
{
handleRoomLost();
2021-08-11 18:56:25 +08:00
return;
}
updateCurrentItem();
addItemButton.Alpha = client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly ? 1 : 0;
Scheduler.AddOnce(UpdateMods);
}
2021-04-09 20:03:45 +08:00
private void updateCurrentItem()
{
Debug.Assert(client.Room != null);
var expectedSelectedItem = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId);
if (expectedSelectedItem == null)
return;
if (SelectedItem.Value?.Equals(expectedSelectedItem) == true && expectedSelectedItem.Beatmap.Value != null)
return;
SelectedItem.Value = null;
if (expectedSelectedItem.Beatmap.Value == null)
{
Task.Run(async () =>
{
var beatmap = await client.GetAPIBeatmap(expectedSelectedItem.BeatmapID).ConfigureAwait(false);
Schedule(() =>
{
expectedSelectedItem.Beatmap.Value = beatmap;
if (Room.Playlist.SingleOrDefault(i => i.ID == client.Room?.Settings.PlaylistItemId)?.Equals(expectedSelectedItem) == true)
applyCurrentItem();
});
});
}
else
applyCurrentItem();
void applyCurrentItem() => SelectedItem.Value = expectedSelectedItem;
}
private void handleRoomLost() => Schedule(() =>
{
if (this.IsCurrentScreen())
this.Exit();
else
ValidForResume = false;
});
private void onLoadRequested()
{
if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable)
return;
// In the case of spectating, IMultiplayerClient.LoadRequested can be fired while the game is still spectating a previous session.
// For now, we want to game to switch to the new game so need to request exiting from the play screen.
if (!ParentScreen.IsCurrentScreen())
2021-04-09 20:03:45 +08:00
{
ParentScreen.MakeCurrent();
2021-04-13 22:21:35 +08:00
Schedule(onLoadRequested);
return;
2021-04-09 20:03:45 +08:00
}
2021-04-22 22:37:33 +08:00
StartPlay();
2020-12-29 15:20:43 +08:00
readyClickOperation?.Dispose();
readyClickOperation = null;
}
2021-04-22 22:37:33 +08:00
protected override Screen CreateGameplayScreen()
{
Debug.Assert(client.LocalUser != null);
Debug.Assert(client.Room != null);
2021-04-22 22:37:33 +08:00
2021-05-03 13:04:20 +08:00
int[] userIds = client.CurrentMatchPlayingUserIds.ToArray();
MultiplayerRoomUser[] users = userIds.Select(id => client.Room.Users.First(u => u.UserID == id)).ToArray();
2021-04-22 22:37:33 +08:00
2021-05-03 13:04:20 +08:00
switch (client.LocalUser.State)
{
case MultiplayerUserState.Spectating:
return new MultiSpectatorScreen(users.Take(PlayerGrid.MAX_PLAYERS).ToArray());
2021-05-03 13:04:20 +08:00
default:
return new PlayerLoader(() => new MultiplayerPlayer(Room, SelectedItem.Value, users));
2021-05-03 13:04:20 +08:00
}
2021-04-22 22:37:33 +08:00
}
public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
{
if (!this.IsCurrentScreen())
return;
if (client.Room == null)
return;
if (!client.IsHost)
{
// todo: should handle this when the request queue is implemented.
// if we decide that the presentation should exit the user from the multiplayer game, the PresentBeatmap
// flow may need to change to support an "unable to present" return value.
return;
}
this.Push(new MultiplayerMatchSongSelect(Room, client.Room.Settings.PlaylistItemId, beatmap, ruleset));
}
2021-08-18 14:54:33 +08:00
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client != null)
{
client.RoomUpdated -= onRoomUpdated;
client.LoadRequested -= onLoadRequested;
}
modSettingChangeTracker?.Dispose();
}
public class AddItemButton : PurpleTriangleButton
{
}
}
}