1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 07:22:55 +08:00

Merge pull request #11239 from smoogipoo/realtime-multiplayer-2

Implement realtime multiplayer
This commit is contained in:
Dean Herbert 2020-12-22 18:23:03 +09:00 committed by GitHub
commit 3c33ea7f1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 2032 additions and 549 deletions

View File

@ -11,7 +11,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi;
using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Screens.Multi.Timeshift;
namespace osu.Game.Tests.Visual.Multiplayer
{
@ -109,14 +109,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("error not displayed", () => !settings.ErrorText.IsPresent);
}
private class TestRoomSettings : MatchSettingsOverlay
private class TestRoomSettings : TimeshiftMatchSettingsOverlay
{
public TriangleButton ApplyButton => Settings.ApplyButton;
public TriangleButton ApplyButton => ((MatchSettings)Settings).ApplyButton;
public OsuTextBox NameField => Settings.NameField;
public OsuDropdown<TimeSpan> DurationField => Settings.DurationField;
public OsuTextBox NameField => ((MatchSettings)Settings).NameField;
public OsuDropdown<TimeSpan> DurationField => ((MatchSettings)Settings).DurationField;
public OsuSpriteText ErrorText => Settings.ErrorText;
public OsuSpriteText ErrorText => ((MatchSettings)Settings).ErrorText;
}
private class TestRoomManager : IRoomManager

View File

@ -10,10 +10,11 @@ using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.Multi.Lounge;
using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Screens.Multi.Timeshift;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneLoungeSubScreen : RoomManagerTestScene
public class TestSceneTimeshiftLoungeSubScreen : RoomManagerTestScene
{
private LoungeSubScreen loungeScreen;
@ -26,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
base.SetUpSteps();
AddStep("push screen", () => LoadScreen(loungeScreen = new LoungeSubScreen
AddStep("push screen", () => LoadScreen(loungeScreen = new TimeshiftLoungeSubScreen
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -16,15 +16,15 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Multi;
using osu.Game.Screens.Multi.Match;
using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Screens.Multi.Timeshift;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchSubScreen : MultiplayerTestScene
public class TestSceneTimeshiftRoomSubScreen : MultiplayerTestScene
{
protected override bool UseOnlineAPI => true;
@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private BeatmapManager manager;
private RulesetStore rulesets;
private TestMatchSubScreen match;
private TestTimeshiftRoomSubScreen match;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUpSteps]
public void SetupSteps()
{
AddStep("load match", () => LoadScreen(match = new TestMatchSubScreen(Room)));
AddStep("load match", () => LoadScreen(match = new TestTimeshiftRoomSubScreen(Room)));
AddUntilStep("wait for load", () => match.IsCurrentScreen());
}
@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create room", () =>
{
InputManager.MoveMouseTo(match.ChildrenOfType<MatchSettingsOverlay.CreateRoomButton>().Single());
InputManager.MoveMouseTo(match.ChildrenOfType<TimeshiftMatchSettingsOverlay.CreateRoomButton>().Single());
InputManager.Click(MouseButton.Left);
});
@ -131,13 +131,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize != 1);
}
private class TestMatchSubScreen : MatchSubScreen
private class TestTimeshiftRoomSubScreen : TimeshiftRoomSubScreen
{
public new Bindable<PlaylistItem> SelectedItem => base.SelectedItem;
public new Bindable<WorkingBeatmap> Beatmap => base.Beatmap;
public TestMatchSubScreen(Room room)
public TestTimeshiftRoomSubScreen(Room room)
: base(room)
{
}

View File

@ -0,0 +1,77 @@
// 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 NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Multi.RealtimeMultiplayer;
using osu.Game.Screens.Multi.RealtimeMultiplayer.Match;
using osu.Game.Tests.Beatmaps;
using osuTK.Input;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class TestSceneRealtimeMatchSubScreen : RealtimeMultiplayerTestScene
{
private RealtimeMatchSubScreen screen;
public TestSceneRealtimeMatchSubScreen()
: base(false)
{
}
[SetUp]
public new void Setup() => Schedule(() =>
{
Room.Name.Value = "Test Room";
});
[SetUpSteps]
public void SetupSteps()
{
AddStep("load match", () => LoadScreen(screen = new RealtimeMatchSubScreen(Room)));
AddUntilStep("wait for load", () => screen.IsCurrentScreen());
}
[Test]
public void TestSettingValidity()
{
AddAssert("create button not enabled", () => !this.ChildrenOfType<RealtimeMatchSettingsOverlay.CreateOrUpdateButton>().Single().Enabled.Value);
AddStep("set playlist", () =>
{
Room.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
});
});
AddAssert("create button enabled", () => this.ChildrenOfType<RealtimeMatchSettingsOverlay.CreateOrUpdateButton>().Single().Enabled.Value);
}
[Test]
public void TestCreatedRoom()
{
AddStep("set playlist", () =>
{
Room.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
});
});
AddStep("click create button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<RealtimeMatchSettingsOverlay.CreateOrUpdateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddWaitStep("wait", 10);
}
}
}

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.Game.Screens.Multi.Components;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class TestSceneRealtimeMultiplayer : RealtimeMultiplayerTestScene
{
public TestSceneRealtimeMultiplayer()
{
var multi = new TestRealtimeMultiplayer();
AddStep("show", () => LoadScreen(multi));
AddUntilStep("wait for loaded", () => multi.IsLoaded);
}
private class TestRealtimeMultiplayer : Screens.Multi.RealtimeMultiplayer.RealtimeMultiplayer
{
protected override RoomManager CreateRoomManager() => new TestRealtimeRoomManager();
}
}
}

View File

@ -11,7 +11,7 @@ using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens.Multi.RealtimeMultiplayer;
using osu.Game.Screens.Multi.RealtimeMultiplayer.Match;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;

View File

@ -64,8 +64,8 @@ namespace osu.Game.Online.Multiplayer
public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets)
{
Beatmap.Value = apiBeatmap.ToBeatmap(rulesets);
Ruleset.Value = rulesets.GetRuleset(RulesetID);
Beatmap.Value ??= apiBeatmap.ToBeatmap(rulesets);
Ruleset.Value ??= rulesets.GetRuleset(RulesetID);
Ruleset rulesetInstance = Ruleset.Value.CreateInstance();

View File

@ -0,0 +1,174 @@
// 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.
#nullable enable
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Online.API;
namespace osu.Game.Online.RealtimeMultiplayer
{
public class RealtimeMultiplayerClient : StatefulMultiplayerClient
{
private const string endpoint = "https://spectator.ppy.sh/multiplayer";
public override IBindable<bool> IsConnected => isConnected;
private readonly Bindable<bool> isConnected = new Bindable<bool>();
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
[Resolved]
private IAPIProvider api { get; set; } = null!;
private HubConnection? connection;
[BackgroundDependencyLoader]
private void load()
{
apiState.BindTo(api.State);
apiState.BindValueChanged(apiStateChanged, true);
}
private void apiStateChanged(ValueChangedEvent<APIState> state)
{
switch (state.NewValue)
{
case APIState.Failing:
case APIState.Offline:
connection?.StopAsync();
connection = null;
break;
case APIState.Online:
Task.Run(Connect);
break;
}
}
protected virtual async Task Connect()
{
if (connection != null)
return;
connection = new HubConnectionBuilder()
.WithUrl(endpoint, options =>
{
options.Headers.Add("Authorization", $"Bearer {api.AccessToken}");
})
.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; })
.Build();
// this is kind of SILLY
// https://github.com/dotnet/aspnetcore/issues/15198
connection.On<MultiplayerRoomState>(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged);
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
connection.On<int>(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
connection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
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.Closed += async ex =>
{
isConnected.Value = false;
if (ex != null)
{
Logger.Log($"Multiplayer client lost connection: {ex}", LoggingTarget.Network);
await tryUntilConnected();
}
};
await tryUntilConnected();
async Task tryUntilConnected()
{
Logger.Log("Multiplayer client connecting...", LoggingTarget.Network);
while (api.State.Value == APIState.Online)
{
try
{
Debug.Assert(connection != null);
// reconnect on any failure
await connection.StartAsync();
Logger.Log("Multiplayer client connected!", LoggingTarget.Network);
// Success.
isConnected.Value = true;
break;
}
catch (Exception e)
{
Logger.Log($"Multiplayer client connection error: {e}", LoggingTarget.Network);
await Task.Delay(5000);
}
}
}
}
protected override Task<MultiplayerRoom> JoinRoom(long roomId)
{
if (!isConnected.Value)
return Task.FromCanceled<MultiplayerRoom>(CancellationToken.None);
return connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoom), roomId);
}
public override async Task LeaveRoom()
{
if (!isConnected.Value)
return;
if (Room == null)
return;
await base.LeaveRoom();
await connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom));
}
public override Task TransferHost(int userId)
{
if (!isConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId);
}
public override Task ChangeSettings(MultiplayerRoomSettings settings)
{
if (!isConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeSettings), settings);
}
public override Task ChangeState(MultiplayerUserState newState)
{
if (!isConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState);
}
public override Task StartMatch()
{
if (!isConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch));
}
}
}

View File

@ -127,10 +127,10 @@ namespace osu.Game.Online.RealtimeMultiplayer
/// </remarks>
/// <param name="name">The new room name, if any.</param>
/// <param name="item">The new room playlist item, if any.</param>
public void ChangeSettings(Optional<string> name = default, Optional<PlaylistItem> item = default)
public Task ChangeSettings(Optional<string> name = default, Optional<PlaylistItem> item = default)
{
if (Room == null)
return;
throw new InvalidOperationException("Must be joined to a match to change settings.");
// A dummy playlist item filled with the current room settings (except mods).
var existingPlaylistItem = new PlaylistItem
@ -146,7 +146,7 @@ namespace osu.Game.Online.RealtimeMultiplayer
RulesetID = Room.Settings.RulesetID
};
ChangeSettings(new MultiplayerRoomSettings
return ChangeSettings(new MultiplayerRoomSettings
{
Name = name.GetOr(Room.Settings.Name),
BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID,

View File

@ -30,6 +30,7 @@ using osu.Game.Database;
using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.IO;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Resources;
@ -78,6 +79,7 @@ namespace osu.Game
protected IAPIProvider API;
private SpectatorStreamingClient spectatorStreaming;
private StatefulMultiplayerClient multiplayerClient;
protected MenuCursorContainer MenuCursorContainer;
@ -211,6 +213,7 @@ namespace osu.Game
dependencies.CacheAs(API ??= new APIAccess(LocalConfig));
dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient());
dependencies.CacheAs(multiplayerClient = new RealtimeMultiplayerClient());
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
@ -277,6 +280,7 @@ namespace osu.Game
if (API is APIAccess apiAccess)
AddInternal(apiAccess);
AddInternal(spectatorStreaming);
AddInternal(multiplayerClient);
AddInternal(RulesetConfigCache);

View File

@ -42,8 +42,8 @@ namespace osu.Game.Screens.Menu
public Action OnBeatmapListing;
public Action OnSolo;
public Action OnSettings;
public Action OnMulti;
public Action OnChart;
public Action OnMultiplayer;
public Action OnTimeshift;
public const float BUTTON_WIDTH = 140f;
public const float WEDGE_WIDTH = 20;
@ -124,8 +124,8 @@ namespace osu.Game.Screens.Menu
private void load(AudioManager audio, IdleTracker idleTracker, GameHost host)
{
buttonsPlay.Add(new Button(@"solo", @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMulti, 0, Key.M));
buttonsPlay.Add(new Button(@"chart", @"button-generic-select", OsuIcon.Charts, new Color4(80, 53, 160, 255), () => OnChart?.Invoke()));
buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
buttonsPlay.Add(new Button(@"timeshift", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onTimeshift, 0, Key.L));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
@ -154,7 +154,7 @@ namespace osu.Game.Screens.Menu
sampleBack = audio.Samples.Get(@"Menu/button-back-select");
}
private void onMulti()
private void onMultiplayer()
{
if (!api.IsLoggedIn)
{
@ -172,7 +172,28 @@ namespace osu.Game.Screens.Menu
return;
}
OnMulti?.Invoke();
OnMultiplayer?.Invoke();
}
private void onTimeshift()
{
if (!api.IsLoggedIn)
{
notifications?.Post(new SimpleNotification
{
Text = "You gotta be logged in to multi 'yo!",
Icon = FontAwesome.Solid.Globe,
Activated = () =>
{
loginOverlay?.Show();
return true;
}
});
return;
}
OnTimeshift?.Invoke();
}
private void updateIdleState(bool isIdle)

View File

@ -17,6 +17,7 @@ using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Multi.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Timeshift;
using osu.Game.Screens.Select;
@ -104,7 +105,8 @@ namespace osu.Game.Screens.Menu
this.Push(new Editor());
},
OnSolo = onSolo,
OnMulti = delegate { this.Push(new TimeshiftMultiplayer()); },
OnMultiplayer = () => this.Push(new RealtimeMultiplayer()),
OnTimeshift = () => this.Push(new TimeshiftMultiplayer()),
OnExit = confirmAndExit,
}
}
@ -136,7 +138,6 @@ namespace osu.Game.Screens.Menu
buttons.OnSettings = () => settings?.ToggleVisibility();
buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility();
buttons.OnChart = () => rankings?.ShowSpotlights();
LoadComponentAsync(background = new BackgroundScreenDefault());
preloadSongSelect();

View File

@ -76,10 +76,7 @@ namespace osu.Game.Screens.Multi.Components
req.Failure += exception =>
{
if (req.Result != null)
onError?.Invoke(req.Result.Error);
else
Logger.Log($"Failed to create the room: {exception}", level: LogLevel.Important);
onError?.Invoke(req.Result?.Error ?? exception.Message);
};
api.Queue(req);

View File

@ -242,7 +242,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
{
multiplayer?.CreateRoom(Room.CreateCopy());
multiplayer?.OpenNewRoom(Room.CreateCopy());
})
};
}

View File

@ -19,16 +19,15 @@ using osu.Game.Users;
namespace osu.Game.Screens.Multi.Lounge
{
[Cached]
public class LoungeSubScreen : MultiplayerSubScreen
public abstract class LoungeSubScreen : MultiplayerSubScreen
{
public override string Title => "Lounge";
protected FilterControl Filter;
protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby();
private readonly IBindable<bool> initialRoomsReceived = new Bindable<bool>();
private FilterControl filter;
private Container content;
private LoadingLayer loadingLayer;
@ -78,11 +77,11 @@ namespace osu.Game.Screens.Multi.Lounge
},
},
},
Filter = new TimeshiftFilterControl
filter = CreateFilterControl().With(d =>
{
RelativeSizeAxes = Axes.X,
Height = 80,
},
d.RelativeSizeAxes = Axes.X;
d.Height = 80;
})
};
// scroll selected room into view on selection.
@ -108,7 +107,7 @@ namespace osu.Game.Screens.Multi.Lounge
content.Padding = new MarginPadding
{
Top = Filter.DrawHeight,
Top = filter.DrawHeight,
Left = WaveOverlayContainer.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING,
Right = WaveOverlayContainer.WIDTH_PADDING + HORIZONTAL_OVERFLOW_PADDING,
};
@ -116,7 +115,7 @@ namespace osu.Game.Screens.Multi.Lounge
protected override void OnFocus(FocusEvent e)
{
Filter.TakeFocus();
filter.TakeFocus();
}
public override void OnEntering(IScreen last)
@ -140,19 +139,19 @@ namespace osu.Game.Screens.Multi.Lounge
private void onReturning()
{
Filter.HoldFocus = true;
filter.HoldFocus = true;
}
public override bool OnExiting(IScreen next)
{
Filter.HoldFocus = false;
filter.HoldFocus = false;
return base.OnExiting(next);
}
public override void OnSuspending(IScreen next)
{
base.OnSuspending(next);
Filter.HoldFocus = false;
filter.HoldFocus = false;
}
private void joinRequested(Room room)
@ -193,7 +192,11 @@ namespace osu.Game.Screens.Multi.Lounge
selectedRoom.Value = room;
this.Push(new MatchSubScreen(room));
this.Push(CreateRoomSubScreen(room));
}
protected abstract FilterControl CreateFilterControl();
protected abstract RoomSubScreen CreateRoomSubScreen(Room room);
}
}

View File

@ -1,385 +1,41 @@
// 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.Specialized;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Multi.Match.Components
{
public class MatchSettingsOverlay : FocusedOverlayContainer
public abstract class MatchSettingsOverlay : FocusedOverlayContainer
{
private const float transition_duration = 350;
private const float field_padding = 45;
protected const float TRANSITION_DURATION = 350;
protected const float FIELD_PADDING = 45;
public Action EditPlaylist;
protected MatchSettings Settings { get; private set; }
protected MultiplayerComposite Settings { get; set; }
[BackgroundDependencyLoader]
private void load()
{
Masking = true;
Child = Settings = new MatchSettings
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y,
EditPlaylist = () => EditPlaylist?.Invoke()
};
}
protected override void PopIn()
{
Settings.MoveToY(0, transition_duration, Easing.OutQuint);
Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint);
}
protected override void PopOut()
{
Settings.MoveToY(-1, transition_duration, Easing.InSine);
Settings.MoveToY(-1, TRANSITION_DURATION, Easing.InSine);
}
protected class MatchSettings : MultiplayerComposite
{
private const float disabled_alpha = 0.2f;
public Action EditPlaylist;
public OsuTextBox NameField, MaxParticipantsField;
public OsuDropdown<TimeSpan> DurationField;
public RoomAvailabilityPicker AvailabilityPicker;
public GameTypePicker TypePicker;
public TriangleButton ApplyButton;
public OsuSpriteText ErrorText;
private OsuSpriteText typeLabel;
private LoadingLayer loadingLayer;
private DrawableRoomPlaylist playlist;
private OsuSpriteText playlistLength;
[Resolved(CanBeNull = true)]
private IRoomManager manager { get; set; }
[Resolved]
private Bindable<Room> currentRoom { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Container dimContent;
InternalChildren = new Drawable[]
{
dimContent = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"28242d"),
},
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.Distributed),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new OsuScrollContainer
{
Padding = new MarginPadding
{
Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING,
Vertical = 10
},
RelativeSizeAxes = Axes.Both,
Children = new[]
{
new Container
{
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new SectionContainer
{
Padding = new MarginPadding { Right = field_padding / 2 },
Children = new[]
{
new Section("Room name")
{
Child = NameField = new SettingsTextBox
{
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
LengthLimit = 100
},
},
new Section("Duration")
{
Child = DurationField = new DurationDropdown
{
RelativeSizeAxes = Axes.X,
Items = new[]
{
TimeSpan.FromMinutes(30),
TimeSpan.FromHours(1),
TimeSpan.FromHours(2),
TimeSpan.FromHours(4),
TimeSpan.FromHours(8),
TimeSpan.FromHours(12),
//TimeSpan.FromHours(16),
TimeSpan.FromHours(24),
TimeSpan.FromDays(3),
TimeSpan.FromDays(7)
}
}
},
new Section("Room visibility")
{
Alpha = disabled_alpha,
Child = AvailabilityPicker = new RoomAvailabilityPicker
{
Enabled = { Value = false }
},
},
new Section("Game type")
{
Alpha = disabled_alpha,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(7),
Children = new Drawable[]
{
TypePicker = new GameTypePicker
{
RelativeSizeAxes = Axes.X,
Enabled = { Value = false }
},
typeLabel = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 14),
Colour = colours.Yellow
},
},
},
},
new Section("Max participants")
{
Alpha = disabled_alpha,
Child = MaxParticipantsField = new SettingsNumberTextBox
{
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
ReadOnly = true,
},
},
new Section("Password (optional)")
{
Alpha = disabled_alpha,
Child = new SettingsPasswordTextBox
{
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
ReadOnly = true,
},
},
},
},
new SectionContainer
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Padding = new MarginPadding { Left = field_padding / 2 },
Children = new[]
{
new Section("Playlist")
{
Child = new GridContainer
{
RelativeSizeAxes = Axes.X,
Height = 300,
Content = new[]
{
new Drawable[]
{
playlist = new DrawableRoomPlaylist(true, true) { RelativeSizeAxes = Axes.Both }
},
new Drawable[]
{
playlistLength = new OsuSpriteText
{
Margin = new MarginPadding { Vertical = 5 },
Colour = colours.Yellow,
Font = OsuFont.GetFont(size: 12),
}
},
new Drawable[]
{
new PurpleTriangleButton
{
RelativeSizeAxes = Axes.X,
Height = 40,
Text = "Edit playlist",
Action = () => EditPlaylist?.Invoke()
}
}
},
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
}
}
},
},
},
},
}
},
},
},
new Drawable[]
{
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Y = 2,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f),
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 20),
Margin = new MarginPadding { Vertical = 20 },
Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING },
Children = new Drawable[]
{
ApplyButton = new CreateRoomButton
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Size = new Vector2(230, 55),
Enabled = { Value = false },
Action = apply,
},
ErrorText = new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Alpha = 0,
Depth = 1,
Colour = colours.RedDark
}
}
}
}
}
}
}
},
}
},
loadingLayer = new LoadingLayer(dimContent)
};
TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue?.Name ?? string.Empty, true);
RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true);
Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true);
Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true);
MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true);
Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true);
playlist.Items.BindTo(Playlist);
Playlist.BindCollectionChanged(onPlaylistChanged, true);
}
protected override void Update()
{
base.Update();
ApplyButton.Enabled.Value = hasValidSettings;
}
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) =>
playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}";
private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0;
private void apply()
{
if (!ApplyButton.Enabled.Value)
return;
hideError();
RoomName.Value = NameField.Text;
Availability.Value = AvailabilityPicker.Current.Value;
Type.Value = TypePicker.Current.Value;
if (int.TryParse(MaxParticipantsField.Text, out int max))
MaxParticipants.Value = max;
else
MaxParticipants.Value = null;
Duration.Value = DurationField.Current.Value;
manager?.CreateRoom(currentRoom.Value, onSuccess, onError);
loadingLayer.Show();
}
private void hideError() => ErrorText.FadeOut(50);
private void onSuccess(Room room) => loadingLayer.Hide();
private void onError(string text)
{
ErrorText.Text = text;
ErrorText.FadeIn(50);
loadingLayer.Hide();
}
}
private class SettingsTextBox : OsuTextBox
protected class SettingsTextBox : OsuTextBox
{
[BackgroundDependencyLoader]
private void load()
@ -389,12 +45,12 @@ namespace osu.Game.Screens.Multi.Match.Components
}
}
private class SettingsNumberTextBox : SettingsTextBox
protected class SettingsNumberTextBox : SettingsTextBox
{
protected override bool CanAddCharacter(char character) => char.IsNumber(character);
}
private class SettingsPasswordTextBox : OsuPasswordTextBox
protected class SettingsPasswordTextBox : OsuPasswordTextBox
{
[BackgroundDependencyLoader]
private void load()
@ -404,7 +60,7 @@ namespace osu.Game.Screens.Multi.Match.Components
}
}
private class SectionContainer : FillFlowContainer<Section>
protected class SectionContainer : FillFlowContainer<Section>
{
public SectionContainer()
{
@ -412,11 +68,11 @@ namespace osu.Game.Screens.Multi.Match.Components
AutoSizeAxes = Axes.Y;
Width = 0.5f;
Direction = FillDirection.Vertical;
Spacing = new Vector2(field_padding);
Spacing = new Vector2(FIELD_PADDING);
}
}
private class Section : Container
protected class Section : Container
{
private readonly Container content;
@ -449,31 +105,5 @@ namespace osu.Game.Screens.Multi.Match.Components
};
}
}
public class CreateRoomButton : TriangleButton
{
public CreateRoomButton()
{
Text = "Create";
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundColour = colours.Yellow;
Triangles.ColourLight = colours.YellowLight;
Triangles.ColourDark = colours.YellowDark;
}
}
private class DurationDropdown : OsuDropdown<TimeSpan>
{
public DurationDropdown()
{
Menu.MaxHeight = 100;
}
protected override string GenerateItemText(TimeSpan item) => item.Humanize();
}
}
}

View File

@ -0,0 +1,74 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Screens.Multi.Match
{
[Cached(typeof(IPreviewTrackOwner))]
public abstract class RoomSubScreen : MultiplayerSubScreen, IPreviewTrackOwner
{
protected readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
public override bool DisallowExternalBeatmapRulesetChanges => true;
[Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
[Resolved]
private BeatmapManager beatmapManager { get; set; }
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
protected override void LoadComplete()
{
base.LoadComplete();
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged));
SelectedItem.Value = Playlist.FirstOrDefault();
managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(beatmapUpdated);
}
private void selectedItemChanged()
{
updateWorkingBeatmap();
var item = SelectedItem.Value;
Mods.Value = item?.RequiredMods?.ToArray() ?? Array.Empty<Mod>();
if (item?.Ruleset != null)
Ruleset.Value = item.Ruleset.Value;
}
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet) => Schedule(updateWorkingBeatmap);
private void updateWorkingBeatmap()
{
var beatmap = SelectedItem.Value?.Beatmap.Value;
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID);
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
}
public override bool OnExiting(IScreen next)
{
RoomManager?.PartRoom();
Mods.Value = Array.Empty<Mod>();
return base.OnExiting(next);
}
}
}

View File

@ -134,7 +134,7 @@ namespace osu.Game.Screens.Multi
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Action = () => CreateRoom()
Action = () => OpenNewRoom()
},
RoomManager = CreateRoomManager()
}
@ -143,7 +143,7 @@ namespace osu.Game.Screens.Multi
screenStack.ScreenPushed += screenPushed;
screenStack.ScreenExited += screenExited;
screenStack.Push(loungeSubScreen = new LoungeSubScreen());
screenStack.Push(loungeSubScreen = CreateLounge());
}
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
@ -264,10 +264,16 @@ namespace osu.Game.Screens.Multi
}
/// <summary>
/// Create a new room.
/// Creates and opens the newly-created room.
/// </summary>
/// <param name="room">An optional template to use when creating the room.</param>
public void CreateRoom(Room room = null) => loungeSubScreen.Open(room ?? new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } });
public void OpenNewRoom(Room room = null) => loungeSubScreen.Open(room ?? CreateNewRoom());
/// <summary>
/// Creates a new room.
/// </summary>
/// <returns>The created <see cref="Room"/>.</returns>
protected virtual Room CreateNewRoom() => new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } };
private void beginHandlingTrack()
{
@ -302,7 +308,7 @@ namespace osu.Game.Screens.Multi
headerBackground.MoveToX(0, MultiplayerSubScreen.X_MOVE_DURATION, Easing.OutQuint);
break;
case MatchSubScreen _:
case RoomSubScreen _:
header.ResizeHeightTo(135, MultiplayerSubScreen.APPEAR_DURATION, Easing.OutQuint);
headerBackground.MoveToX(-MultiplayerSubScreen.X_SHIFT, MultiplayerSubScreen.X_MOVE_DURATION, Easing.OutQuint);
break;
@ -324,7 +330,7 @@ namespace osu.Game.Screens.Multi
private void updateTrack(ValueChangedEvent<WorkingBeatmap> _ = null)
{
if (screenStack.CurrentScreen is MatchSubScreen)
if (screenStack.CurrentScreen is RoomSubScreen)
{
var track = Beatmap.Value?.Track;
@ -355,6 +361,8 @@ namespace osu.Game.Screens.Multi
protected abstract RoomManager CreateRoomManager();
protected abstract LoungeSubScreen CreateLounge();
private class MultiplayerWaveContainer : WaveContainer
{
protected override bool StartHidden => true;

View File

@ -25,9 +25,11 @@ namespace osu.Game.Screens.Multi.Play
public Action Exited;
[Resolved(typeof(Room), nameof(Room.RoomID))]
private Bindable<int?> roomId { get; set; }
protected Bindable<int?> RoomId { get; private set; }
private readonly PlaylistItem playlistItem;
protected readonly PlaylistItem PlaylistItem;
protected int? Token { get; private set; }
[Resolved]
private IAPIProvider api { get; set; }
@ -35,32 +37,31 @@ namespace osu.Game.Screens.Multi.Play
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
public TimeshiftPlayer(PlaylistItem playlistItem)
public TimeshiftPlayer(PlaylistItem playlistItem, bool allowPause = true)
: base(allowPause)
{
this.playlistItem = playlistItem;
PlaylistItem = playlistItem;
}
private int? token;
[BackgroundDependencyLoader]
private void load()
{
token = null;
Token = null;
bool failed = false;
// Sanity checks to ensure that TimeshiftPlayer matches the settings for the current PlaylistItem
if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != playlistItem.Beatmap.Value.OnlineBeatmapID)
if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != PlaylistItem.Beatmap.Value.OnlineBeatmapID)
throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap");
if (ruleset.Value.ID != playlistItem.Ruleset.Value.ID)
if (ruleset.Value.ID != PlaylistItem.Ruleset.Value.ID)
throw new InvalidOperationException("Current Ruleset does not match PlaylistItem's Ruleset");
if (!playlistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals)))
if (!PlaylistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals)))
throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods");
var req = new CreateRoomScoreRequest(roomId.Value ?? 0, playlistItem.ID, Game.VersionHash);
req.Success += r => token = r.ID;
var req = new CreateRoomScoreRequest(RoomId.Value ?? 0, PlaylistItem.ID, Game.VersionHash);
req.Success += r => Token = r.ID;
req.Failure += e =>
{
failed = true;
@ -76,7 +77,7 @@ namespace osu.Game.Screens.Multi.Play
api.Queue(req);
while (!failed && !token.HasValue)
while (!failed && !Token.HasValue)
Thread.Sleep(1000);
}
@ -92,8 +93,8 @@ namespace osu.Game.Screens.Multi.Play
protected override ResultsScreen CreateResults(ScoreInfo score)
{
Debug.Assert(roomId.Value != null);
return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true);
Debug.Assert(RoomId.Value != null);
return new TimeshiftResultsScreen(score, RoomId.Value.Value, PlaylistItem, true);
}
protected override Score CreateScore()
@ -107,10 +108,10 @@ namespace osu.Game.Screens.Multi.Play
{
await base.SubmitScore(score);
Debug.Assert(token != null);
Debug.Assert(Token != null);
var tcs = new TaskCompletionSource<bool>();
var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score.ScoreInfo);
var request = new SubmitRoomScoreRequest(Token.Value, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo);
request.Success += s =>
{

View File

@ -32,8 +32,8 @@ namespace osu.Game.Screens.Multi.Ranking
[Resolved]
private IAPIProvider api { get; set; }
public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry)
: base(score, allowRetry)
public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true)
: base(score, allowRetry, allowWatchingReplay)
{
this.roomId = roomId;
this.playlistItem = playlistItem;

View File

@ -0,0 +1,81 @@
// 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.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
using osu.Game.Online.API;
using osu.Game.Screens.Multi.Match.Components;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match
{
public class BeatmapSelectionControl : MultiplayerComposite
{
[Resolved]
private RealtimeMatchSubScreen matchSubScreen { get; set; }
[Resolved]
private IAPIProvider api { get; set; }
private Container beatmapPanelContainer;
private Button selectButton;
public BeatmapSelectionControl()
{
AutoSizeAxes = Axes.Y;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
beatmapPanelContainer = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
},
selectButton = new PurpleTriangleButton
{
RelativeSizeAxes = Axes.X,
Height = 40,
Text = "Select beatmap",
Action = () => matchSubScreen.Push(new RealtimeMatchSongSelect()),
Alpha = 0
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Playlist.BindCollectionChanged(onPlaylistChanged, true);
Host.BindValueChanged(host =>
{
if (RoomID.Value == null || host.NewValue?.Equals(api.LocalUser.Value) == true)
selectButton.Show();
else
selectButton.Hide();
}, true);
}
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (Playlist.Any())
beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(Playlist.Single(), false, false);
else
beatmapPanelContainer.Clear();
}
}
}

View File

@ -0,0 +1,48 @@
// 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.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osuTK;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match
{
public class RealtimeMatchFooter : CompositeDrawable
{
public const float HEIGHT = 50;
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
private readonly Drawable background;
public RealtimeMatchFooter()
{
RelativeSizeAxes = Axes.X;
Height = HEIGHT;
InternalChildren = new[]
{
background = new Box { RelativeSizeAxes = Axes.Both },
new RealtimeReadyButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(600, 50),
SelectedItem = { BindTarget = SelectedItem }
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
background.Colour = Color4Extensions.FromHex(@"28242d");
}
}
}

View File

@ -0,0 +1,106 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Users.Drawables;
using osuTK;
using FontWeight = osu.Game.Graphics.FontWeight;
using OsuColour = osu.Game.Graphics.OsuColour;
using OsuFont = osu.Game.Graphics.OsuFont;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match
{
public class RealtimeMatchHeader : MultiplayerComposite
{
public const float HEIGHT = 50;
public Action OpenSettings;
private UpdateableAvatar avatar;
private LinkFlowContainer hostText;
private Button openSettingsButton;
[Resolved]
private IAPIProvider api { get; set; }
public RealtimeMatchHeader()
{
RelativeSizeAxes = Axes.X;
Height = HEIGHT;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
avatar = new UpdateableAvatar
{
Size = new Vector2(50),
Masking = true,
CornerRadius = 10,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 30),
Current = { BindTarget = RoomName }
},
hostText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 20))
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
}
}
}
}
},
openSettingsButton = new PurpleTriangleButton
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Size = new Vector2(150, HEIGHT),
Text = "Open settings",
Action = () => OpenSettings?.Invoke(),
Alpha = 0
}
};
Host.BindValueChanged(host =>
{
avatar.User = host.NewValue;
hostText.Clear();
if (host.NewValue != null)
{
hostText.AddText("hosted by ");
hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
}
openSettingsButton.Alpha = host.NewValue?.Equals(api.LocalUser.Value) == true ? 1 : 0;
}, true);
}
}
}

View File

@ -0,0 +1,357 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Screens.Multi.Match.Components;
using osuTK;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match
{
public class RealtimeMatchSettingsOverlay : MatchSettingsOverlay
{
[BackgroundDependencyLoader]
private void load()
{
Child = Settings = new MatchSettings
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y,
SettingsApplied = Hide
};
}
protected class MatchSettings : MultiplayerComposite
{
private const float disabled_alpha = 0.2f;
public Action SettingsApplied;
public OsuTextBox NameField, MaxParticipantsField;
public RoomAvailabilityPicker AvailabilityPicker;
public GameTypePicker TypePicker;
public TriangleButton ApplyButton;
public OsuSpriteText ErrorText;
private OsuSpriteText typeLabel;
private LoadingLayer loadingLayer;
private BeatmapSelectionControl initialBeatmapControl;
[Resolved]
private IRoomManager manager { get; set; }
[Resolved]
private StatefulMultiplayerClient client { get; set; }
[Resolved]
private Bindable<Room> currentRoom { get; set; }
[Resolved]
private Bindable<WorkingBeatmap> beatmap { get; set; }
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Container dimContent;
InternalChildren = new Drawable[]
{
dimContent = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"28242d"),
},
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.Distributed),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new OsuScrollContainer
{
Padding = new MarginPadding
{
Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING,
Vertical = 10
},
RelativeSizeAxes = Axes.Both,
Children = new[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new SectionContainer
{
Padding = new MarginPadding { Right = FIELD_PADDING / 2 },
Children = new[]
{
new Section("Room name")
{
Child = NameField = new SettingsTextBox
{
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
},
},
new Section("Room visibility")
{
Alpha = disabled_alpha,
Child = AvailabilityPicker = new RoomAvailabilityPicker
{
Enabled = { Value = false }
},
},
new Section("Game type")
{
Alpha = disabled_alpha,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(7),
Children = new Drawable[]
{
TypePicker = new GameTypePicker
{
RelativeSizeAxes = Axes.X,
Enabled = { Value = false }
},
typeLabel = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 14),
Colour = colours.Yellow
},
},
},
},
},
},
new SectionContainer
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Padding = new MarginPadding { Left = FIELD_PADDING / 2 },
Children = new[]
{
new Section("Max participants")
{
Alpha = disabled_alpha,
Child = MaxParticipantsField = new SettingsNumberTextBox
{
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
ReadOnly = true,
},
},
new Section("Password (optional)")
{
Alpha = disabled_alpha,
Child = new SettingsPasswordTextBox
{
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
ReadOnly = true,
},
},
}
}
},
},
initialBeatmapControl = new BeatmapSelectionControl
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
Width = 0.5f
}
}
}
},
},
},
new Drawable[]
{
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Y = 2,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f),
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 20),
Margin = new MarginPadding { Vertical = 20 },
Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING },
Children = new Drawable[]
{
ApplyButton = new CreateOrUpdateButton
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Size = new Vector2(230, 55),
Enabled = { Value = false },
Action = apply,
},
ErrorText = new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Alpha = 0,
Depth = 1,
Colour = colours.RedDark
}
}
}
}
}
}
}
},
}
},
loadingLayer = new LoadingLayer(dimContent)
};
TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue?.Name ?? string.Empty, true);
RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true);
Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true);
Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true);
MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true);
RoomID.BindValueChanged(roomId => initialBeatmapControl.Alpha = roomId.NewValue == null ? 1 : 0, true);
}
protected override void Update()
{
base.Update();
ApplyButton.Enabled.Value = Playlist.Count > 0 && NameField.Text.Length > 0;
}
private void apply()
{
if (!ApplyButton.Enabled.Value)
return;
hideError();
loadingLayer.Show();
// If the client is already in a room, update via the client.
// Otherwise, update the room directly in preparation for it to be submitted to the API on match creation.
if (client.Room != null)
{
client.ChangeSettings(name: NameField.Text).ContinueWith(t => Schedule(() =>
{
if (t.IsCompletedSuccessfully)
onSuccess(currentRoom.Value);
else
onError(t.Exception?.Message ?? "Error changing settings.");
}));
}
else
{
currentRoom.Value.Name.Value = NameField.Text;
currentRoom.Value.Availability.Value = AvailabilityPicker.Current.Value;
currentRoom.Value.Type.Value = TypePicker.Current.Value;
if (int.TryParse(MaxParticipantsField.Text, out int max))
currentRoom.Value.MaxParticipants.Value = max;
else
currentRoom.Value.MaxParticipants.Value = null;
manager?.CreateRoom(currentRoom.Value, onSuccess, onError);
}
}
private void hideError() => ErrorText.FadeOut(50);
private void onSuccess(Room room)
{
loadingLayer.Hide();
SettingsApplied?.Invoke();
}
private void onError(string text)
{
ErrorText.Text = text;
ErrorText.FadeIn(50);
loadingLayer.Hide();
}
}
public class CreateOrUpdateButton : TriangleButton
{
[Resolved(typeof(Room), nameof(Room.RoomID))]
private Bindable<int?> roomId { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
roomId.BindValueChanged(id => Text = id.NewValue == null ? "Create" : "Update", true);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundColour = colours.Yellow;
Triangles.ColourLight = colours.YellowLight;
Triangles.ColourDark = colours.YellowDark;
}
}
}
}

View File

@ -15,7 +15,7 @@ using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Components;
using osuTK;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match
{
public class RealtimeReadyButton : RealtimeRoomComposite
{

View File

@ -0,0 +1,31 @@
// 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.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Components;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
{
public class ParticipantsListHeader : OverlinedHeader
{
[Resolved]
private StatefulMultiplayerClient client { get; set; }
public ParticipantsListHeader()
: base("Participants")
{
}
protected override void Update()
{
base.Update();
var room = client.Room;
if (room == null)
return;
Details.Value = room.Users.Count.ToString();
}
}
}

View File

@ -0,0 +1,17 @@
// 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.Game.Screens.Multi.Lounge.Components;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{
public class RealtimeFilterControl : FilterControl
{
protected override FilterCriteria CreateCriteria()
{
var criteria = base.CreateCriteria();
criteria.Category = "realtime";
return criteria;
}
}
}

View File

@ -0,0 +1,17 @@
// 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.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Lounge;
using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Screens.Multi.Match;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{
public class RealtimeLoungeSubScreen : LoungeSubScreen
{
protected override FilterControl CreateFilterControl() => new RealtimeFilterControl();
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new RealtimeMatchSubScreen(room);
}
}

View File

@ -0,0 +1,84 @@
// 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 Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Select;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{
public class RealtimeMatchSongSelect : SongSelect, IMultiplayerSubScreen
{
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;
public RealtimeMatchSongSelect()
{
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(loadingLayer = new LoadingLayer(Carousel));
}
protected override bool OnStart()
{
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)
{
loadingLayer.Show();
client.ChangeSettings(item: item).ContinueWith(t =>
{
return Schedule(() =>
{
loadingLayer.Hide();
if (t.IsCompletedSuccessfully)
this.Exit();
else
Logger.Log($"Could not use current beatmap ({t.Exception?.Message})", level: LogLevel.Important);
});
});
}
else
{
playlist.Clear();
playlist.Add(item);
this.Exit();
}
return true;
}
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
}
}

View File

@ -0,0 +1,201 @@
// 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.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Multi.Match;
using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Screens.Multi.RealtimeMultiplayer.Match;
using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants;
using osu.Game.Screens.Play;
using osu.Game.Users;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{
[Cached]
public class RealtimeMatchSubScreen : RoomSubScreen
{
public override string Title { get; }
public override string ShortTitle => "match";
[Resolved(canBeNull: true)]
private Multiplayer multiplayer { get; set; }
[Resolved]
private StatefulMultiplayerClient client { get; set; }
private RealtimeMatchSettingsOverlay settingsOverlay;
public RealtimeMatchSubScreen(Room room)
{
Title = room.RoomID.Value == null ? "New match" : room.Name.Value;
Activity.Value = new UserActivity.InLobby(room);
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Horizontal = 105,
Vertical = 20
},
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
},
Content = new[]
{
new Drawable[]
{
new RealtimeMatchHeader
{
OpenSettings = () => settingsOverlay.Show()
}
},
new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 5, Vertical = 10 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[] { new ParticipantsListHeader() },
new Drawable[]
{
new Participants.ParticipantsList
{
RelativeSizeAxes = Axes.Both
},
}
}
}
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 5 },
Children = new Drawable[]
{
new OverlinedHeader("Beatmap"),
new BeatmapSelectionControl { RelativeSizeAxes = Axes.X }
}
}
}
}
}
},
new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[] { new OverlinedHeader("Chat") },
new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } }
}
}
}
},
}
}
},
new Drawable[]
{
new RealtimeMatchFooter { SelectedItem = { BindTarget = SelectedItem } }
}
},
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
}
},
settingsOverlay = new RealtimeMatchSettingsOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = client.Room == null ? Visibility.Visible : Visibility.Hidden }
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Playlist.BindCollectionChanged(onPlaylistChanged, true);
client.LoadRequested += onLoadRequested;
}
public override bool OnBackButton()
{
if (client.Room != null && settingsOverlay.State.Value == Visibility.Visible)
{
settingsOverlay.Hide();
return true;
}
return base.OnBackButton();
}
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault();
private void onLoadRequested() => multiplayer?.Push(new PlayerLoader(() => new RealtimePlayer(SelectedItem.Value)));
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client != null)
client.LoadRequested -= onLoadRequested;
}
}
}

View File

@ -0,0 +1,67 @@
// 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.Logging;
using osu.Framework.Screens;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Multi.Lounge;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{
public class RealtimeMultiplayer : Multiplayer
{
[Resolved]
private StatefulMultiplayerClient client { get; set; }
public override void OnResuming(IScreen last)
{
base.OnResuming(last);
if (client.Room != null)
client.ChangeState(MultiplayerUserState.Idle);
}
protected override void UpdatePollingRate(bool isIdle)
{
var timeshiftManager = (RealtimeRoomManager)RoomManager;
if (!this.IsCurrentScreen())
{
timeshiftManager.TimeBetweenListingPolls.Value = 0;
timeshiftManager.TimeBetweenSelectionPolls.Value = 0;
}
else
{
switch (CurrentSubScreen)
{
case LoungeSubScreen _:
timeshiftManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000;
timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000;
break;
// Don't poll inside the match or anywhere else.
default:
timeshiftManager.TimeBetweenListingPolls.Value = 0;
timeshiftManager.TimeBetweenSelectionPolls.Value = 0;
break;
}
}
Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})");
}
protected override Room CreateNewRoom()
{
var room = base.CreateNewRoom();
room.Category.Value = RoomCategory.Realtime;
return room;
}
protected override RoomManager CreateRoomManager() => new RealtimeRoomManager();
protected override LoungeSubScreen CreateLounge() => new RealtimeLoungeSubScreen();
}
}

View File

@ -0,0 +1,92 @@
// 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.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Scoring;
using osu.Game.Screens.Multi.Play;
using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{
// Todo: The "room" part of TimeshiftPlayer should be split out into an abstract player class to be inherited instead.
public class RealtimePlayer : TimeshiftPlayer
{
protected override bool PauseOnFocusLost => false;
// Disallow fails in multiplayer for now.
protected override bool CheckModsAllowFailure() => false;
[Resolved]
private StatefulMultiplayerClient client { get; set; }
private readonly TaskCompletionSource<bool> resultsReady = new TaskCompletionSource<bool>();
private readonly ManualResetEventSlim startedEvent = new ManualResetEventSlim();
public RealtimePlayer(PlaylistItem playlistItem)
: base(playlistItem, false)
{
}
[BackgroundDependencyLoader]
private void load()
{
if (Token == null)
return; // Todo: Somehow handle token retrieval failure.
client.MatchStarted += onMatchStarted;
client.ResultsReady += onResultsReady;
client.ChangeState(MultiplayerUserState.Loaded);
if (!startedEvent.Wait(TimeSpan.FromSeconds(30)))
{
Logger.Log("Failed to start the multiplayer match in time.", LoggingTarget.Runtime, LogLevel.Important);
Schedule(() =>
{
ValidForResume = false;
this.Exit();
});
}
}
private void onMatchStarted() => startedEvent.Set();
private void onResultsReady() => resultsReady.SetResult(true);
protected override async Task SubmitScore(Score score)
{
await base.SubmitScore(score);
await client.ChangeState(MultiplayerUserState.FinishedPlay);
// Await up to 30 seconds for results to become available (3 api request timeouts).
// This is arbitrary just to not leave the player in an essentially deadlocked state if any connection issues occur.
await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(30)));
}
protected override ResultsScreen CreateResults(ScoreInfo score)
{
Debug.Assert(RoomId.Value != null);
return new RealtimeResultsScreen(score, RoomId.Value.Value, PlaylistItem);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client != null)
{
client.MatchStarted -= onMatchStarted;
client.ResultsReady -= onResultsReady;
}
}
}
}

View File

@ -0,0 +1,17 @@
// 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.Game.Online.Multiplayer;
using osu.Game.Scoring;
using osu.Game.Screens.Multi.Ranking;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{
public class RealtimeResultsScreen : TimeshiftResultsScreen
{
public RealtimeResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem, false, false)
{
}
}
}

View File

@ -38,10 +38,10 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
}
public override void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
=> base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError);
=> base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError);
public override void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
=> base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError);
=> base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError);
public override void PartRoom()
{
@ -62,17 +62,18 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
});
}
private void joinMultiplayerRoom(Room room, Action<Room> onSuccess = null)
private void joinMultiplayerRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
{
Debug.Assert(room.RoomID.Value != null);
var joinTask = multiplayerClient.JoinRoom(room);
joinTask.ContinueWith(_ => onSuccess?.Invoke(room), TaskContinuationOptions.OnlyOnRanToCompletion);
joinTask.ContinueWith(_ => Schedule(() => onSuccess?.Invoke(room)), TaskContinuationOptions.OnlyOnRanToCompletion);
joinTask.ContinueWith(t =>
{
PartRoom();
if (t.Exception != null)
Logger.Error(t.Exception, "Failed to join multiplayer room.");
Schedule(() => onError?.Invoke(t.Exception?.ToString() ?? string.Empty));
}, TaskContinuationOptions.NotOnRanToCompletion);
}

View File

@ -0,0 +1,17 @@
// 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.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Lounge;
using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Screens.Multi.Match;
namespace osu.Game.Screens.Multi.Timeshift
{
public class TimeshiftLoungeSubScreen : LoungeSubScreen
{
protected override FilterControl CreateFilterControl() => new TimeshiftFilterControl();
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new TimeshiftRoomSubScreen(room);
}
}

View File

@ -0,0 +1,391 @@
// 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.Specialized;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osu.Game.Screens.Multi.Match.Components;
using osuTK;
namespace osu.Game.Screens.Multi.Timeshift
{
public class TimeshiftMatchSettingsOverlay : MatchSettingsOverlay
{
public Action EditPlaylist;
[BackgroundDependencyLoader]
private void load()
{
Child = Settings = new MatchSettings
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y,
EditPlaylist = () => EditPlaylist?.Invoke()
};
}
protected class MatchSettings : MultiplayerComposite
{
private const float disabled_alpha = 0.2f;
public Action EditPlaylist;
public OsuTextBox NameField, MaxParticipantsField;
public OsuDropdown<TimeSpan> DurationField;
public RoomAvailabilityPicker AvailabilityPicker;
public GameTypePicker TypePicker;
public TriangleButton ApplyButton;
public OsuSpriteText ErrorText;
private OsuSpriteText typeLabel;
private LoadingLayer loadingLayer;
private DrawableRoomPlaylist playlist;
private OsuSpriteText playlistLength;
[Resolved(CanBeNull = true)]
private IRoomManager manager { get; set; }
[Resolved]
private Bindable<Room> currentRoom { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Container dimContent;
InternalChildren = new Drawable[]
{
dimContent = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"28242d"),
},
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.Distributed),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new OsuScrollContainer
{
Padding = new MarginPadding
{
Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING,
Vertical = 10
},
RelativeSizeAxes = Axes.Both,
Children = new[]
{
new Container
{
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new SectionContainer
{
Padding = new MarginPadding { Right = FIELD_PADDING / 2 },
Children = new[]
{
new Section("Room name")
{
Child = NameField = new SettingsTextBox
{
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
LengthLimit = 100
},
},
new Section("Duration")
{
Child = DurationField = new DurationDropdown
{
RelativeSizeAxes = Axes.X,
Items = new[]
{
TimeSpan.FromMinutes(30),
TimeSpan.FromHours(1),
TimeSpan.FromHours(2),
TimeSpan.FromHours(4),
TimeSpan.FromHours(8),
TimeSpan.FromHours(12),
//TimeSpan.FromHours(16),
TimeSpan.FromHours(24),
TimeSpan.FromDays(3),
TimeSpan.FromDays(7)
}
}
},
new Section("Room visibility")
{
Alpha = disabled_alpha,
Child = AvailabilityPicker = new RoomAvailabilityPicker
{
Enabled = { Value = false }
},
},
new Section("Game type")
{
Alpha = disabled_alpha,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(7),
Children = new Drawable[]
{
TypePicker = new GameTypePicker
{
RelativeSizeAxes = Axes.X,
Enabled = { Value = false }
},
typeLabel = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 14),
Colour = colours.Yellow
},
},
},
},
new Section("Max participants")
{
Alpha = disabled_alpha,
Child = MaxParticipantsField = new SettingsNumberTextBox
{
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
ReadOnly = true,
},
},
new Section("Password (optional)")
{
Alpha = disabled_alpha,
Child = new SettingsPasswordTextBox
{
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
ReadOnly = true,
},
},
},
},
new SectionContainer
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Padding = new MarginPadding { Left = FIELD_PADDING / 2 },
Children = new[]
{
new Section("Playlist")
{
Child = new GridContainer
{
RelativeSizeAxes = Axes.X,
Height = 300,
Content = new[]
{
new Drawable[]
{
playlist = new DrawableRoomPlaylist(true, true) { RelativeSizeAxes = Axes.Both }
},
new Drawable[]
{
playlistLength = new OsuSpriteText
{
Margin = new MarginPadding { Vertical = 5 },
Colour = colours.Yellow,
Font = OsuFont.GetFont(size: 12),
}
},
new Drawable[]
{
new PurpleTriangleButton
{
RelativeSizeAxes = Axes.X,
Height = 40,
Text = "Edit playlist",
Action = () => EditPlaylist?.Invoke()
}
}
},
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
}
}
},
},
},
},
}
},
},
},
new Drawable[]
{
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Y = 2,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f),
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 20),
Margin = new MarginPadding { Vertical = 20 },
Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING },
Children = new Drawable[]
{
ApplyButton = new CreateRoomButton
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Size = new Vector2(230, 55),
Enabled = { Value = false },
Action = apply,
},
ErrorText = new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Alpha = 0,
Depth = 1,
Colour = colours.RedDark
}
}
}
}
}
}
}
},
}
},
loadingLayer = new LoadingLayer(dimContent)
};
TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue?.Name ?? string.Empty, true);
RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true);
Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true);
Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true);
MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true);
Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true);
playlist.Items.BindTo(Playlist);
Playlist.BindCollectionChanged(onPlaylistChanged, true);
}
protected override void Update()
{
base.Update();
ApplyButton.Enabled.Value = hasValidSettings;
}
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) =>
playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}";
private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0;
private void apply()
{
if (!ApplyButton.Enabled.Value)
return;
hideError();
RoomName.Value = NameField.Text;
Availability.Value = AvailabilityPicker.Current.Value;
Type.Value = TypePicker.Current.Value;
if (int.TryParse(MaxParticipantsField.Text, out int max))
MaxParticipants.Value = max;
else
MaxParticipants.Value = null;
Duration.Value = DurationField.Current.Value;
manager?.CreateRoom(currentRoom.Value, onSuccess, onError);
loadingLayer.Show();
}
private void hideError() => ErrorText.FadeOut(50);
private void onSuccess(Room room) => loadingLayer.Hide();
private void onError(string text)
{
ErrorText.Text = text;
ErrorText.FadeIn(50);
loadingLayer.Hide();
}
}
public class CreateRoomButton : TriangleButton
{
public CreateRoomButton()
{
Text = "Create";
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundColour = colours.Yellow;
Triangles.ColourLight = colours.YellowLight;
Triangles.ColourDark = colours.YellowDark;
}
}
private class DurationDropdown : OsuDropdown<TimeSpan>
{
public DurationDropdown()
{
Menu.MaxHeight = 100;
}
protected override string GenerateItemText(TimeSpan item) => item.Humanize();
}
}
}

View File

@ -29,7 +29,7 @@ namespace osu.Game.Screens.Multi.Timeshift
timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000;
break;
case MatchSubScreen _:
case RoomSubScreen _:
timeshiftManager.TimeBetweenListingPolls.Value = 0;
timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 30000 : 5000;
break;
@ -45,5 +45,7 @@ namespace osu.Game.Screens.Multi.Timeshift
}
protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager();
protected override LoungeSubScreen CreateLounge() => new TimeshiftLoungeSubScreen();
}
}

View File

@ -1,7 +1,6 @@
// 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.
using System;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
@ -9,13 +8,10 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.GameTypes;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Multi.Match;
using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Screens.Multi.Play;
using osu.Game.Screens.Multi.Ranking;
@ -24,13 +20,10 @@ using osu.Game.Screens.Select;
using osu.Game.Users;
using Footer = osu.Game.Screens.Multi.Match.Components.Footer;
namespace osu.Game.Screens.Multi.Match
namespace osu.Game.Screens.Multi.Timeshift
{
[Cached(typeof(IPreviewTrackOwner))]
public class MatchSubScreen : MultiplayerSubScreen, IPreviewTrackOwner
public class TimeshiftRoomSubScreen : RoomSubScreen
{
public override bool DisallowExternalBeatmapRulesetChanges => true;
public override string Title { get; }
public override string ShortTitle => "room";
@ -38,27 +31,15 @@ namespace osu.Game.Screens.Multi.Match
[Resolved(typeof(Room), nameof(Room.RoomID))]
private Bindable<int?> roomId { get; set; }
[Resolved(typeof(Room), nameof(Room.Type))]
private Bindable<GameType> type { get; set; }
[Resolved(typeof(Room), nameof(Room.Playlist))]
private BindableList<PlaylistItem> playlist { get; set; }
[Resolved]
private BeatmapManager beatmapManager { get; set; }
[Resolved(canBeNull: true)]
private Multiplayer multiplayer { get; set; }
protected readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
private MatchSettingsOverlay settingsOverlay;
private MatchLeaderboard leaderboard;
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
private OverlinedHeader participantsHeader;
public MatchSubScreen(Room room)
public TimeshiftRoomSubScreen(Room room)
{
Title = room.RoomID.Value == null ? "New room" : room.Name.Value;
Activity.Value = new UserActivity.InLobby(room);
@ -96,7 +77,7 @@ namespace osu.Game.Screens.Multi.Match
},
Content = new[]
{
new Drawable[] { new Components.Header() },
new Drawable[] { new Match.Components.Header() },
new Drawable[]
{
participantsHeader = new OverlinedHeader("Participants")
@ -141,7 +122,7 @@ namespace osu.Game.Screens.Multi.Match
new DrawableRoomPlaylistWithResults
{
RelativeSizeAxes = Axes.Both,
Items = { BindTarget = playlist },
Items = { BindTarget = Playlist },
SelectedItem = { BindTarget = SelectedItem },
RequestShowResults = item =>
{
@ -208,7 +189,7 @@ namespace osu.Game.Screens.Multi.Match
new Dimension(GridSizeMode.AutoSize),
}
},
settingsOverlay = new MatchSettingsOverlay
settingsOverlay = new TimeshiftMatchSettingsOverlay
{
RelativeSizeAxes = Axes.Both,
EditPlaylist = () => this.Push(new MatchSongSelect()),
@ -234,61 +215,17 @@ namespace osu.Game.Screens.Multi.Match
// Set the first playlist item.
// This is scheduled since updating the room and playlist may happen in an arbitrary order (via Room.CopyFrom()).
Schedule(() => SelectedItem.Value = playlist.FirstOrDefault());
Schedule(() => SelectedItem.Value = Playlist.FirstOrDefault());
}
}, true);
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged));
SelectedItem.Value = playlist.FirstOrDefault();
managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(beatmapUpdated);
}
public override bool OnExiting(IScreen next)
{
RoomManager?.PartRoom();
Mods.Value = Array.Empty<Mod>();
return base.OnExiting(next);
}
private void selectedItemChanged()
{
updateWorkingBeatmap();
var item = SelectedItem.Value;
Mods.Value = item?.RequiredMods?.ToArray() ?? Array.Empty<Mod>();
if (item?.Ruleset != null)
Ruleset.Value = item.Ruleset.Value;
}
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet) => Schedule(updateWorkingBeatmap);
private void updateWorkingBeatmap()
{
var beatmap = SelectedItem.Value?.Beatmap.Value;
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID);
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
}
private void onStart()
{
switch (type.Value)
multiplayer?.Push(new PlayerLoader(() => new TimeshiftPlayer(SelectedItem.Value)
{
default:
case GameTypeTimeshift _:
multiplayer?.Push(new PlayerLoader(() => new TimeshiftPlayer(SelectedItem.Value)
{
Exited = () => leaderboard.RefreshScores()
}));
break;
}
Exited = () => leaderboard.RefreshScores()
}));
}
}
}

View File

@ -57,11 +57,13 @@ namespace osu.Game.Screens.Ranking
private APIRequest nextPageRequest;
private readonly bool allowRetry;
private readonly bool allowWatchingReplay;
protected ResultsScreen(ScoreInfo score, bool allowRetry)
protected ResultsScreen(ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true)
{
Score = score;
this.allowRetry = allowRetry;
this.allowWatchingReplay = allowWatchingReplay;
SelectedScore.Value = score;
}
@ -128,15 +130,7 @@ namespace osu.Game.Screens.Ranking
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new ReplayDownloadButton(null)
{
Score = { BindTarget = SelectedScore },
Width = 300
},
}
Direction = FillDirection.Horizontal
}
}
}
@ -157,6 +151,15 @@ namespace osu.Game.Screens.Ranking
ScorePanelList.AddScore(Score, shouldFlair);
}
if (allowWatchingReplay)
{
buttons.Add(new ReplayDownloadButton(null)
{
Score = { BindTarget = SelectedScore },
Width = 300
});
}
if (player != null && allowRetry)
{
buttons.Add(new RetryButton { Width = 300 });

View File

@ -7,8 +7,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi;
using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Screens.Multi.RealtimeMultiplayer;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer
[Cached(typeof(StatefulMultiplayerClient))]
public TestRealtimeMultiplayerClient Client { get; }
[Cached(typeof(RealtimeRoomManager))]
[Cached(typeof(IRoomManager))]
public TestRealtimeRoomManager RoomManager { get; }
[Cached]

View File

@ -6,8 +6,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi;
using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Screens.Multi.RealtimeMultiplayer;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer
[Cached(typeof(StatefulMultiplayerClient))]
public readonly TestRealtimeMultiplayerClient Client;
[Cached(typeof(RealtimeRoomManager))]
[Cached(typeof(IRoomManager))]
public readonly TestRealtimeRoomManager RoomManager;
[Cached]