1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 21:47:25 +08:00

Merge branch 'master' into disallow-multiplayer-restart-retry

This commit is contained in:
Dean Herbert 2020-12-24 13:31:54 +09:00
commit 76935b93b6
21 changed files with 361 additions and 132 deletions

View File

@ -0,0 +1,26 @@
// 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.Threading.Tasks;
using osu.Framework.Logging;
namespace osu.Game.Extensions
{
public static class TaskExtensions
{
/// <summary>
/// Denote a task which is to be run without local error handling logic, where failure is not catastrophic.
/// Avoids unobserved exceptions from being fired.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="logOnError">Whether errors should be logged as important, or silently ignored.</param>
public static void CatchUnobservedExceptions(this Task task, bool logOnError = false)
{
task.ContinueWith(t =>
{
if (logOnError)
Logger.Log($"Error running task: {t.Exception?.Message ?? "unknown"}", LoggingTarget.Runtime, LogLevel.Important);
}, TaskContinuationOptions.NotOnRanToCompletion);
}
}
}

View File

@ -122,7 +122,7 @@ namespace osu.Game.Online.RealtimeMultiplayer
protected override Task<MultiplayerRoom> JoinRoom(long roomId) protected override Task<MultiplayerRoom> JoinRoom(long roomId)
{ {
if (!isConnected.Value) if (!isConnected.Value)
return Task.FromCanceled<MultiplayerRoom>(CancellationToken.None); return Task.FromCanceled<MultiplayerRoom>(new CancellationToken(true));
return connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoom), roomId); return connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoom), roomId);
} }
@ -130,7 +130,11 @@ namespace osu.Game.Online.RealtimeMultiplayer
public override async Task LeaveRoom() public override async Task LeaveRoom()
{ {
if (!isConnected.Value) if (!isConnected.Value)
{
// even if not connected, make sure the local room state can be cleaned up.
await base.LeaveRoom();
return; return;
}
if (Room == null) if (Room == null)
return; return;

View File

@ -11,8 +11,10 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -75,6 +77,19 @@ namespace osu.Game.Online.RealtimeMultiplayer
// Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise. // Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise.
private int playlistItemId; private int playlistItemId;
protected StatefulMultiplayerClient()
{
IsConnected.BindValueChanged(connected =>
{
// clean up local room state on server disconnect.
if (!connected.NewValue)
{
Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important);
LeaveRoom().CatchUnobservedExceptions();
}
});
}
/// <summary> /// <summary>
/// Joins the <see cref="MultiplayerRoom"/> for a given API <see cref="Room"/>. /// Joins the <see cref="MultiplayerRoom"/> for a given API <see cref="Room"/>.
/// </summary> /// </summary>
@ -354,7 +369,6 @@ namespace osu.Game.Online.RealtimeMultiplayer
if (Room == null) if (Room == null)
return; return;
// Update a few properties of the room instantaneously.
Schedule(() => Schedule(() =>
{ {
if (Room == null) if (Room == null)
@ -362,6 +376,7 @@ namespace osu.Game.Online.RealtimeMultiplayer
Debug.Assert(apiRoom != null); Debug.Assert(apiRoom != null);
// Update a few properties of the room instantaneously.
Room.Settings = settings; Room.Settings = settings;
apiRoom.Name.Value = Room.Settings.Name; apiRoom.Name.Value = Room.Settings.Name;
@ -370,12 +385,12 @@ namespace osu.Game.Online.RealtimeMultiplayer
apiRoom.Playlist.Clear(); apiRoom.Playlist.Clear();
RoomChanged?.Invoke(); RoomChanged?.Invoke();
var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId);
req.Success += res => updatePlaylist(settings, res);
api.Queue(req);
}); });
var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId);
req.Success += res => updatePlaylist(settings, res);
api.Queue(req);
} }
private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet) private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet)

View File

@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Scoring
} }
/// <summary> /// <summary>
/// Given a minimal set of inputs, return the computed score and accuracy for the tracked beatmap / mods combination. /// Given a minimal set of inputs, return the computed score and accuracy for the tracked beatmap / mods combination, at the current point in time.
/// </summary> /// </summary>
/// <param name="mode">The <see cref="ScoringMode"/> to compute the total score in.</param> /// <param name="mode">The <see cref="ScoringMode"/> to compute the total score in.</param>
/// <param name="maxCombo">The maximum combo achievable in the beatmap.</param> /// <param name="maxCombo">The maximum combo achievable in the beatmap.</param>
@ -252,15 +252,28 @@ namespace osu.Game.Rulesets.Scoring
computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value; computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value;
} }
double accuracy = calculateAccuracyRatio(computedBaseScore); double pointInTimeAccuracy = calculateAccuracyRatio(computedBaseScore, true);
double comboRatio = calculateComboRatio(maxCombo); double comboRatio = calculateComboRatio(maxCombo);
double score = GetScore(mode, maxAchievableCombo, accuracy, comboRatio, scoreResultCounts); double score = GetScore(mode, maxAchievableCombo, calculateAccuracyRatio(computedBaseScore), comboRatio, scoreResultCounts);
return (score, accuracy); return (score, pointInTimeAccuracy);
}
/// <summary>
/// Get the accuracy fraction for the provided base score.
/// </summary>
/// <param name="baseScore">The score to be used for accuracy calculation.</param>
/// <param name="preferRolling">Whether the rolling base score should be used (ie. for the current point in time based on Apply/Reverted results).</param>
/// <returns>The computed accuracy.</returns>
private double calculateAccuracyRatio(double baseScore, bool preferRolling = false)
{
if (preferRolling && rollingMaxBaseScore != 0)
return baseScore / rollingMaxBaseScore;
return maxBaseScore > 0 ? baseScore / maxBaseScore : 0;
} }
private double calculateAccuracyRatio(double baseScore) => maxBaseScore > 0 ? baseScore / maxBaseScore : 0;
private double calculateComboRatio(int maxCombo) => maxAchievableCombo > 0 ? (double)maxCombo / maxAchievableCombo : 1; private double calculateComboRatio(int maxCombo) => maxAchievableCombo > 0 ? (double)maxCombo / maxAchievableCombo : 1;
private double getBonusScore(Dictionary<HitResult, int> statistics) private double getBonusScore(Dictionary<HitResult, int> statistics)

View File

@ -184,7 +184,7 @@ namespace osu.Game.Screens.Multi.Lounge
/// <summary> /// <summary>
/// Push a room as a new subscreen. /// Push a room as a new subscreen.
/// </summary> /// </summary>
public void Open(Room room) public virtual void Open(Room room)
{ {
// Handles the case where a room is clicked 3 times in quick succession // Handles the case where a room is clicked 3 times in quick succession
if (!this.IsCurrentScreen()) if (!this.IsCurrentScreen())

View File

@ -9,6 +9,7 @@ using osu.Framework.Screens;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Screens.Multi.Match namespace osu.Game.Screens.Multi.Match
@ -23,6 +24,9 @@ namespace osu.Game.Screens.Multi.Match
[Resolved(typeof(Room), nameof(Room.Playlist))] [Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList<PlaylistItem> Playlist { get; private set; } protected BindableList<PlaylistItem> Playlist { get; private set; }
[Resolved]
private MusicController music { get; set; }
[Resolved] [Resolved]
private BeatmapManager beatmapManager { get; set; } private BeatmapManager beatmapManager { get; set; }
@ -39,6 +43,34 @@ namespace osu.Game.Screens.Multi.Match
managerUpdated.BindValueChanged(beatmapUpdated); managerUpdated.BindValueChanged(beatmapUpdated);
} }
public override void OnEntering(IScreen last)
{
base.OnEntering(last);
beginHandlingTrack();
}
public override void OnSuspending(IScreen next)
{
endHandlingTrack();
base.OnSuspending(next);
}
public override void OnResuming(IScreen last)
{
base.OnResuming(last);
beginHandlingTrack();
}
public override bool OnExiting(IScreen next)
{
RoomManager?.PartRoom();
Mods.Value = Array.Empty<Mod>();
endHandlingTrack();
return base.OnExiting(next);
}
private void selectedItemChanged() private void selectedItemChanged()
{ {
updateWorkingBeatmap(); updateWorkingBeatmap();
@ -63,12 +95,42 @@ namespace osu.Game.Screens.Multi.Match
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
} }
public override bool OnExiting(IScreen next) private void beginHandlingTrack()
{ {
RoomManager?.PartRoom(); Beatmap.BindValueChanged(applyLoopingToTrack, true);
Mods.Value = Array.Empty<Mod>(); }
return base.OnExiting(next); private void endHandlingTrack()
{
Beatmap.ValueChanged -= applyLoopingToTrack;
cancelTrackLooping();
}
private void applyLoopingToTrack(ValueChangedEvent<WorkingBeatmap> _ = null)
{
if (!this.IsCurrentScreen())
return;
var track = Beatmap.Value?.Track;
if (track != null)
{
track.RestartPoint = Beatmap.Value.Metadata.PreviewTime;
track.Looping = true;
music?.EnsurePlayingSomething();
}
}
private void cancelTrackLooping()
{
var track = Beatmap?.Value?.Track;
if (track != null)
{
track.Looping = false;
track.RestartPoint = 0;
}
} }
} }
} }

View File

@ -9,7 +9,6 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -22,7 +21,6 @@ using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge;
using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Screens.Multi.Match; using osu.Game.Screens.Multi.Match;
using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Users; using osu.Game.Users;
using osuTK; using osuTK;
@ -130,12 +128,18 @@ namespace osu.Game.Screens.Multi
} }
}, },
new Header(screenStack), new Header(screenStack),
createButton = new CreateRoomButton createButton = CreateNewMultiplayerGameButton().With(button =>
{ {
Anchor = Anchor.TopRight, button.Anchor = Anchor.TopRight;
Origin = Anchor.TopRight, button.Origin = Anchor.TopRight;
Action = () => OpenNewRoom() button.Size = new Vector2(150, Header.HEIGHT - 20);
}, button.Margin = new MarginPadding
{
Top = 10,
Right = 10 + HORIZONTAL_OVERFLOW_PADDING,
};
button.Action = () => OpenNewRoom();
}),
RoomManager = CreateRoomManager() RoomManager = CreateRoomManager()
} }
}; };
@ -198,8 +202,6 @@ namespace osu.Game.Screens.Multi
{ {
this.FadeIn(); this.FadeIn();
waves.Show(); waves.Show();
beginHandlingTrack();
} }
public override void OnResuming(IScreen last) public override void OnResuming(IScreen last)
@ -209,8 +211,6 @@ namespace osu.Game.Screens.Multi
base.OnResuming(last); base.OnResuming(last);
beginHandlingTrack();
UpdatePollingRate(isIdle.Value); UpdatePollingRate(isIdle.Value);
} }
@ -219,8 +219,6 @@ namespace osu.Game.Screens.Multi
this.ScaleTo(1.1f, 250, Easing.InSine); this.ScaleTo(1.1f, 250, Easing.InSine);
this.FadeOut(250); this.FadeOut(250);
endHandlingTrack();
UpdatePollingRate(isIdle.Value); UpdatePollingRate(isIdle.Value);
} }
@ -235,8 +233,6 @@ namespace osu.Game.Screens.Multi
if (screenStack.CurrentScreen != null) if (screenStack.CurrentScreen != null)
loungeSubScreen.MakeCurrent(); loungeSubScreen.MakeCurrent();
endHandlingTrack();
base.OnExiting(next); base.OnExiting(next);
return false; return false;
} }
@ -275,17 +271,6 @@ namespace osu.Game.Screens.Multi
/// <returns>The created <see cref="Room"/>.</returns> /// <returns>The created <see cref="Room"/>.</returns>
protected virtual Room CreateNewRoom() => new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } }; protected virtual Room CreateNewRoom() => new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } };
private void beginHandlingTrack()
{
Beatmap.BindValueChanged(updateTrack, true);
}
private void endHandlingTrack()
{
cancelLooping();
Beatmap.ValueChanged -= updateTrack;
}
private void screenPushed(IScreen lastScreen, IScreen newScreen) private void screenPushed(IScreen lastScreen, IScreen newScreen)
{ {
subScreenChanged(lastScreen, newScreen); subScreenChanged(lastScreen, newScreen);
@ -322,47 +307,16 @@ namespace osu.Game.Screens.Multi
UpdatePollingRate(isIdle.Value); UpdatePollingRate(isIdle.Value);
createButton.FadeTo(newScreen is LoungeSubScreen ? 1 : 0, 200); createButton.FadeTo(newScreen is LoungeSubScreen ? 1 : 0, 200);
updateTrack();
} }
protected IScreen CurrentSubScreen => screenStack.CurrentScreen; protected IScreen CurrentSubScreen => screenStack.CurrentScreen;
private void updateTrack(ValueChangedEvent<WorkingBeatmap> _ = null)
{
if (screenStack.CurrentScreen is RoomSubScreen)
{
var track = Beatmap.Value?.Track;
if (track != null)
{
track.RestartPoint = Beatmap.Value.Metadata.PreviewTime;
track.Looping = true;
music?.EnsurePlayingSomething();
}
}
else
{
cancelLooping();
}
}
private void cancelLooping()
{
var track = Beatmap?.Value?.Track;
if (track != null)
{
track.Looping = false;
track.RestartPoint = 0;
}
}
protected abstract RoomManager CreateRoomManager(); protected abstract RoomManager CreateRoomManager();
protected abstract LoungeSubScreen CreateLounge(); protected abstract LoungeSubScreen CreateLounge();
protected abstract OsuButton CreateNewMultiplayerGameButton();
private class MultiplayerWaveContainer : WaveContainer private class MultiplayerWaveContainer : WaveContainer
{ {
protected override bool StartHidden => true; protected override bool StartHidden => true;
@ -385,26 +339,5 @@ namespace osu.Game.Screens.Multi
protected override double TransformDuration => 200; protected override double TransformDuration => 200;
} }
} }
public class CreateRoomButton : PurpleTriangleButton
{
public CreateRoomButton()
{
Size = new Vector2(150, Header.HEIGHT - 20);
Margin = new MarginPadding
{
Top = 10,
Right = 10 + HORIZONTAL_OVERFLOW_PADDING,
};
}
[BackgroundDependencyLoader]
private void load()
{
Triangles.TriangleScale = 1.5f;
Text = "Create room";
}
}
} }
} }

View File

@ -0,0 +1,23 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Match.Components;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{
public class CreateRealtimeMatchButton : PurpleTriangleButton
{
[BackgroundDependencyLoader]
private void load(StatefulMultiplayerClient multiplayerClient)
{
Triangles.TriangleScale = 1.5f;
Text = "Create match";
((IBindable<bool>)Enabled).BindTo(multiplayerClient.IsConnected);
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -299,7 +300,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match
if (t.IsCompletedSuccessfully) if (t.IsCompletedSuccessfully)
onSuccess(currentRoom.Value); onSuccess(currentRoom.Value);
else else
onError(t.Exception?.Message ?? "Error changing settings."); onError(t.Exception?.AsSingular().Message ?? "Error changing settings.");
})); }));
} }
else else

View File

@ -7,6 +7,7 @@ using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Extensions;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -105,13 +106,13 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match
return; return;
if (localUser.State == MultiplayerUserState.Idle) if (localUser.State == MultiplayerUserState.Idle)
Client.ChangeState(MultiplayerUserState.Ready); Client.ChangeState(MultiplayerUserState.Ready).CatchUnobservedExceptions(true);
else else
{ {
if (Room?.Host?.Equals(localUser) == true) if (Room?.Host?.Equals(localUser) == true)
Client.StartMatch(); Client.StartMatch().CatchUnobservedExceptions(true);
else else
Client.ChangeState(MultiplayerUserState.Idle); Client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true);
} }
} }

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Extensions;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -176,7 +177,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
if (Room.Host?.UserID != api.LocalUser.Value.Id) if (Room.Host?.UserID != api.LocalUser.Value.Id)
return; return;
Client.TransferHost(targetUser); Client.TransferHost(targetUser).CatchUnobservedExceptions(true);
}) })
}; };
} }

View File

@ -1,7 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge;
using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Screens.Multi.Match; using osu.Game.Screens.Multi.Match;
@ -13,5 +16,19 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
protected override FilterControl CreateFilterControl() => new RealtimeFilterControl(); protected override FilterControl CreateFilterControl() => new RealtimeFilterControl();
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new RealtimeMatchSubScreen(room); protected override RoomSubScreen CreateRoomSubScreen(Room room) => new RealtimeMatchSubScreen(room);
[Resolved]
private StatefulMultiplayerClient client { get; set; }
public override void Open(Room room)
{
if (!client.IsConnected.Value)
{
Logger.Log("Not currently connected to the multiplayer server.", LoggingTarget.Runtime, LogLevel.Important);
return;
}
base.Open(room);
}
} }
} }

View File

@ -58,14 +58,17 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
client.ChangeSettings(item: item).ContinueWith(t => client.ChangeSettings(item: item).ContinueWith(t =>
{ {
return Schedule(() => Schedule(() =>
{ {
loadingLayer.Hide(); loadingLayer.Hide();
if (t.IsCompletedSuccessfully) if (t.IsCompletedSuccessfully)
this.Exit(); this.Exit();
else else
{
Logger.Log($"Could not use current beatmap ({t.Exception?.Message})", level: LogLevel.Important); Logger.Log($"Could not use current beatmap ({t.Exception?.Message})", level: LogLevel.Important);
Carousel.AllowSelection = true;
}
}); });
}); });
} }

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
@ -16,6 +18,7 @@ using osu.Game.Screens.Multi.RealtimeMultiplayer.Match;
using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants; using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Users; using osu.Game.Users;
using ParticipantsList = osu.Game.Screens.Multi.RealtimeMultiplayer.Participants.ParticipantsList;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{ {
@ -34,6 +37,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
private RealtimeMatchSettingsOverlay settingsOverlay; private RealtimeMatchSettingsOverlay settingsOverlay;
private IBindable<bool> isConnected;
public RealtimeMatchSubScreen(Room room) public RealtimeMatchSubScreen(Room room)
{ {
Title = room.RoomID.Value == null ? "New match" : room.Name.Value; Title = room.RoomID.Value == null ? "New match" : room.Name.Value;
@ -102,7 +107,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
new Drawable[] { new ParticipantsListHeader() }, new Drawable[] { new ParticipantsListHeader() },
new Drawable[] new Drawable[]
{ {
new Participants.ParticipantsList new ParticipantsList
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
@ -173,6 +178,13 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
Playlist.BindCollectionChanged(onPlaylistChanged, true); Playlist.BindCollectionChanged(onPlaylistChanged, true);
client.LoadRequested += onLoadRequested; client.LoadRequested += onLoadRequested;
isConnected = client.IsConnected.GetBoundCopy();
isConnected.BindValueChanged(connected =>
{
if (!connected.NewValue)
Schedule(this.Exit);
}, true);
} }
public override bool OnBackButton() public override bool OnBackButton()
@ -188,7 +200,14 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault(); private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault();
private void onLoadRequested() => multiplayer?.Push(new PlayerLoader(() => new RealtimePlayer(SelectedItem.Value))); private void onLoadRequested()
{
Debug.Assert(client.Room != null);
int[] userIds = client.Room.Users.Where(u => u.State >= MultiplayerUserState.WaitingForLoad).Select(u => u.UserID).ToArray();
multiplayer?.Push(new PlayerLoader(() => new RealtimePlayer(SelectedItem.Value, userIds)));
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {

View File

@ -4,6 +4,8 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Components;
@ -21,7 +23,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
base.OnResuming(last); base.OnResuming(last);
if (client.Room != null) if (client.Room != null)
client.ChangeState(MultiplayerUserState.Idle); client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true);
} }
protected override void UpdatePollingRate(bool isIdle) protected override void UpdatePollingRate(bool isIdle)
@ -63,5 +65,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
protected override RoomManager CreateRoomManager() => new RealtimeRoomManager(); protected override RoomManager CreateRoomManager() => new RealtimeRoomManager();
protected override LoungeSubScreen CreateLounge() => new RealtimeLoungeSubScreen(); protected override LoungeSubScreen CreateLounge() => new RealtimeLoungeSubScreen();
protected override OsuButton CreateNewMultiplayerGameButton() => new CreateRealtimeMatchButton();
} }
} }

View File

@ -5,15 +5,18 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Multi.Play; using osu.Game.Screens.Multi.Play;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osuTK;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{ {
@ -28,16 +31,29 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
[Resolved] [Resolved]
private StatefulMultiplayerClient client { get; set; } private StatefulMultiplayerClient client { get; set; }
private IBindable<bool> isConnected;
private readonly TaskCompletionSource<bool> resultsReady = new TaskCompletionSource<bool>(); private readonly TaskCompletionSource<bool> resultsReady = new TaskCompletionSource<bool>();
private readonly ManualResetEventSlim startedEvent = new ManualResetEventSlim(); private readonly ManualResetEventSlim startedEvent = new ManualResetEventSlim();
public RealtimePlayer(PlaylistItem playlistItem) [CanBeNull]
private MultiplayerGameplayLeaderboard leaderboard;
private readonly int[] userIds;
/// <summary>
/// Construct a multiplayer player.
/// </summary>
/// <param name="playlistItem">The playlist item to be played.</param>
/// <param name="userIds">The users which are participating in this game.</param>
public RealtimePlayer(PlaylistItem playlistItem, int[] userIds)
: base(playlistItem, new PlayerConfiguration : base(playlistItem, new PlayerConfiguration
{ {
AllowPause = false, AllowPause = false,
AllowRestart = false, AllowRestart = false,
}) })
{ {
this.userIds = userIds;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -48,18 +64,57 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
client.MatchStarted += onMatchStarted; client.MatchStarted += onMatchStarted;
client.ResultsReady += onResultsReady; client.ResultsReady += onResultsReady;
client.ChangeState(MultiplayerUserState.Loaded);
isConnected = client.IsConnected.GetBoundCopy();
isConnected.BindValueChanged(connected =>
{
if (!connected.NewValue)
{
// messaging to the user about this disconnect will be provided by the RealtimeMatchSubScreen.
failAndBail();
}
}, true);
client.ChangeState(MultiplayerUserState.Loaded)
.ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion);
if (!startedEvent.Wait(TimeSpan.FromSeconds(30))) if (!startedEvent.Wait(TimeSpan.FromSeconds(30)))
{ {
Logger.Log("Failed to start the multiplayer match in time.", LoggingTarget.Runtime, LogLevel.Important); failAndBail("Failed to start the multiplayer match in time.");
return;
Schedule(() =>
{
ValidForResume = false;
this.Exit();
});
} }
Debug.Assert(client.Room != null);
// todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area.
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add);
}
private void failAndBail(string message = null)
{
if (!string.IsNullOrEmpty(message))
Logger.Log(message, LoggingTarget.Runtime, LogLevel.Important);
startedEvent.Set();
Schedule(() => PerformExit(false));
}
protected override void Update()
{
base.Update();
adjustLeaderboardPosition();
}
private void adjustLeaderboardPosition()
{
if (leaderboard == null)
return;
const float padding = 44; // enough margin to avoid the hit error display.
leaderboard.Position = new Vector2(
padding,
padding + HUDOverlay.TopScoringElementsHeight);
} }
private void onMatchStarted() => startedEvent.Set(); private void onMatchStarted() => startedEvent.Set();

View File

@ -7,7 +7,9 @@ using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Extensions;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.RoomStatuses; using osu.Game.Online.Multiplayer.RoomStatuses;
using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Online.RealtimeMultiplayer;
@ -43,6 +45,12 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
public override void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null) public override void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
{ {
if (!multiplayerClient.IsConnected.Value)
{
onError?.Invoke("Not currently connected to the multiplayer server.");
return;
}
// this is done here as a pre-check to avoid clicking on already closed rooms in the lounge from triggering a server join. // this is done here as a pre-check to avoid clicking on already closed rooms in the lounge from triggering a server join.
// should probably be done at a higher level, but due to the current structure of things this is the easiest place for now. // should probably be done at a higher level, but due to the current structure of things this is the easiest place for now.
if (room.Status.Value is RoomStatusEnded) if (room.Status.Value is RoomStatusEnded)
@ -62,7 +70,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
var joinedRoom = JoinedRoom.Value; var joinedRoom = JoinedRoom.Value;
base.PartRoom(); base.PartRoom();
multiplayerClient.LeaveRoom();
multiplayerClient.LeaveRoom().CatchUnobservedExceptions();
// Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case. // Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case.
// This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling. // This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling.
@ -77,15 +86,21 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{ {
Debug.Assert(room.RoomID.Value != null); Debug.Assert(room.RoomID.Value != null);
var joinTask = multiplayerClient.JoinRoom(room); multiplayerClient.JoinRoom(room).ContinueWith(t =>
joinTask.ContinueWith(_ => Schedule(() => onSuccess?.Invoke(room)), TaskContinuationOptions.OnlyOnRanToCompletion);
joinTask.ContinueWith(t =>
{ {
PartRoom(); if (t.IsCompletedSuccessfully)
if (t.Exception != null) Schedule(() => onSuccess?.Invoke(room));
Logger.Error(t.Exception, "Failed to join multiplayer room."); else
Schedule(() => onError?.Invoke(t.Exception?.ToString() ?? string.Empty)); {
}, TaskContinuationOptions.NotOnRanToCompletion); const string message = "Failed to join multiplayer room.";
if (t.Exception != null)
Logger.Error(t.Exception, message);
PartRoom();
Schedule(() => onError?.Invoke(t.Exception?.AsSingular().Message ?? message));
}
});
} }
private void updatePolling() private void updatePolling()

View File

@ -0,0 +1,19 @@
// 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.Allocation;
using osu.Game.Screens.Multi.Match.Components;
namespace osu.Game.Screens.Multi.Timeshift
{
public class CreateTimeshiftRoomButton : PurpleTriangleButton
{
[BackgroundDependencyLoader]
private void load()
{
Triangles.TriangleScale = 1.5f;
Text = "Create room";
}
}
}

View File

@ -3,6 +3,7 @@
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge;
using osu.Game.Screens.Multi.Match; using osu.Game.Screens.Multi.Match;
@ -47,5 +48,7 @@ namespace osu.Game.Screens.Multi.Timeshift
protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager(); protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager();
protected override LoungeSubScreen CreateLounge() => new TimeshiftLoungeSubScreen(); protected override LoungeSubScreen CreateLounge() => new TimeshiftLoungeSubScreen();
protected override OsuButton CreateNewMultiplayerGameButton() => new CreateTimeshiftRoomButton();
} }
} }

View File

@ -28,6 +28,11 @@ namespace osu.Game.Screens.Play
public const Easing FADE_EASING = Easing.Out; public const Easing FADE_EASING = Easing.Out;
/// <summary>
/// The total height of all the top of screen scoring elements.
/// </summary>
public float TopScoringElementsHeight { get; private set; }
public readonly KeyCounterDisplay KeyCounter; public readonly KeyCounterDisplay KeyCounter;
public readonly SkinnableComboCounter ComboCounter; public readonly SkinnableComboCounter ComboCounter;
public readonly SkinnableScoreCounter ScoreCounter; public readonly SkinnableScoreCounter ScoreCounter;
@ -209,7 +214,7 @@ namespace osu.Game.Screens.Play
// HACK: for now align with the accuracy counter. // HACK: for now align with the accuracy counter.
// this is done for the sake of hacky legacy skins which extend the health bar to take up the full screen area. // this is done for the sake of hacky legacy skins which extend the health bar to take up the full screen area.
// it only works with the default skin due to padding offsetting it *just enough* to coexist. // it only works with the default skin due to padding offsetting it *just enough* to coexist.
topRightElements.Y = ToLocalSpace(AccuracyCounter.Drawable.ScreenSpaceDrawQuad.BottomRight).Y; topRightElements.Y = TopScoringElementsHeight = ToLocalSpace(AccuracyCounter.Drawable.ScreenSpaceDrawQuad.BottomRight).Y;
bottomRightElements.Y = -Progress.Height; bottomRightElements.Y = -Progress.Height;
} }

View File

@ -374,7 +374,7 @@ namespace osu.Game.Screens.Play
if (!this.IsCurrentScreen()) return; if (!this.IsCurrentScreen()) return;
fadeOut(true); fadeOut(true);
performImmediateExit(); PerformExit(true);
}, },
}, },
failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, },
@ -463,20 +463,30 @@ namespace osu.Game.Screens.Play
return playable; return playable;
} }
private void performImmediateExit() /// <summary>
/// Exits the <see cref="Player"/>.
/// </summary>
/// <param name="userRequested">
/// Whether the exit is requested by the user, or a higher-level game component.
/// Pausing is allowed only in the former case.
/// </param>
protected void PerformExit(bool userRequested)
{ {
// if a restart has been requested, cancel any pending completion (user has shown intent to restart). // if a restart has been requested, cancel any pending completion (user has shown intent to restart).
completionProgressDelegate?.Cancel(); completionProgressDelegate?.Cancel();
ValidForResume = false; ValidForResume = false;
performUserRequestedExit(); if (!this.IsCurrentScreen()) return;
if (userRequested)
performUserRequestedExit();
else
this.Exit();
} }
private void performUserRequestedExit() private void performUserRequestedExit()
{ {
if (!this.IsCurrentScreen()) return;
if (ValidForResume && HasFailed && !FailOverlay.IsPresent) if (ValidForResume && HasFailed && !FailOverlay.IsPresent)
{ {
failAnimation.FinishTransforms(true); failAnimation.FinishTransforms(true);
@ -506,7 +516,7 @@ namespace osu.Game.Screens.Play
RestartRequested?.Invoke(); RestartRequested?.Invoke();
if (this.IsCurrentScreen()) if (this.IsCurrentScreen())
performImmediateExit(); PerformExit(true);
else else
this.MakeCurrent(); this.MakeCurrent();
} }