Merge branch 'master' into fix-editor-crash-on-nan-scale

This commit is contained in:
Bartłomiej Dach
2020-12-22 20:46:45 +01:00
committed by GitHub
49 changed files with 2120 additions and 638 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1214.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.1222.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -5,11 +5,11 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ {
@ -56,31 +56,30 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
(animation as IFramedAnimation)?.GotoFrame(0); (animation as IFramedAnimation)?.GotoFrame(0);
this.FadeInFromZero(20, Easing.Out)
.Then().Delay(160)
.FadeOutFromOne(40, Easing.In);
switch (result) switch (result)
{ {
case HitResult.None: case HitResult.None:
break; break;
case HitResult.Miss: case HitResult.Miss:
animation.ScaleTo(1.6f); animation.ScaleTo(1.2f).Then().ScaleTo(1, 100, Easing.Out);
animation.ScaleTo(1, 100, Easing.In);
animation.MoveTo(Vector2.Zero);
animation.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
animation.RotateTo(0); animation.RotateTo(0);
animation.RotateTo(40, 800, Easing.InQuint); animation.RotateTo(RNG.NextSingle(-5.73f, 5.73f), 100, Easing.Out);
this.FadeOutFromOne(800);
break; break;
default: default:
animation.ScaleTo(0.8f); animation.ScaleTo(0.8f)
animation.ScaleTo(1, 250, Easing.OutElastic); .Then().ScaleTo(1, 40)
// this is actually correct to match stable; there were overlapping transforms.
animation.Delay(50).ScaleTo(0.75f, 250); .Then().ScaleTo(0.85f)
.Then().ScaleTo(0.7f, 40)
this.Delay(50).FadeOut(200); .Then().Delay(100)
.Then().ScaleTo(0.4f, 40, Easing.In);
break; break;
} }
} }

View File

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

View File

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

View File

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

View File

@ -64,8 +64,8 @@ namespace osu.Game.Online.Multiplayer
public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets) public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets)
{ {
Beatmap.Value = apiBeatmap.ToBeatmap(rulesets); Beatmap.Value ??= apiBeatmap.ToBeatmap(rulesets);
Ruleset.Value = rulesets.GetRuleset(RulesetID); Ruleset.Value ??= rulesets.GetRuleset(RulesetID);
Ruleset rulesetInstance = Ruleset.Value.CreateInstance(); 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> /// </remarks>
/// <param name="name">The new room name, if any.</param> /// <param name="name">The new room name, if any.</param>
/// <param name="item">The new room playlist item, 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) 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). // A dummy playlist item filled with the current room settings (except mods).
var existingPlaylistItem = new PlaylistItem var existingPlaylistItem = new PlaylistItem
@ -146,7 +146,7 @@ namespace osu.Game.Online.RealtimeMultiplayer
RulesetID = Room.Settings.RulesetID RulesetID = Room.Settings.RulesetID
}; };
ChangeSettings(new MultiplayerRoomSettings return ChangeSettings(new MultiplayerRoomSettings
{ {
Name = name.GetOr(Room.Settings.Name), Name = name.GetOr(Room.Settings.Name),
BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID, BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID,

View File

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

View File

@ -1,7 +1,7 @@
// 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 System.Collections.Generic; using System;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -25,9 +25,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private FillFlowContainer<SettingsSlider<float>> scalingSettings; private FillFlowContainer<SettingsSlider<float>> scalingSettings;
private readonly IBindable<Display> currentDisplay = new Bindable<Display>();
private readonly IBindableList<WindowMode> windowModes = new BindableList<WindowMode>();
private Bindable<ScalingMode> scalingMode; private Bindable<ScalingMode> scalingMode;
private Bindable<Size> sizeFullscreen; private Bindable<Size> sizeFullscreen;
private readonly IBindableList<WindowMode> windowModes = new BindableList<WindowMode>();
private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) });
[Resolved] [Resolved]
private OsuGameBase game { get; set; } private OsuGameBase game { get; set; }
@ -53,22 +57,25 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingPositionY = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionY); scalingPositionY = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionY);
if (host.Window != null) if (host.Window != null)
{
currentDisplay.BindTo(host.Window.CurrentDisplayBindable);
windowModes.BindTo(host.Window.SupportedWindowModes); windowModes.BindTo(host.Window.SupportedWindowModes);
}
Container resolutionSettingsContainer;
Children = new Drawable[] Children = new Drawable[]
{ {
windowModeDropdown = new SettingsDropdown<WindowMode> windowModeDropdown = new SettingsDropdown<WindowMode>
{ {
LabelText = "Screen mode", LabelText = "Screen mode",
Current = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode),
ItemSource = windowModes, ItemSource = windowModes,
Current = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode),
}, },
resolutionSettingsContainer = new Container resolutionDropdown = new ResolutionSettingsDropdown
{ {
RelativeSizeAxes = Axes.X, LabelText = "Resolution",
AutoSizeAxes = Axes.Y ShowsDefaultIndicator = false,
ItemSource = resolutions,
Current = sizeFullscreen
}, },
new SettingsSlider<float, UIScaleSlider> new SettingsSlider<float, UIScaleSlider>
{ {
@ -126,32 +133,34 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}, },
}; };
scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); windowModes.BindCollectionChanged((sender, args) =>
var resolutions = getResolutions();
if (resolutions.Count > 1)
{ {
resolutionSettingsContainer.Child = resolutionDropdown = new ResolutionSettingsDropdown if (windowModes.Count > 1)
{ windowModeDropdown.Show();
LabelText = "Resolution",
ShowsDefaultIndicator = false,
Items = resolutions,
Current = sizeFullscreen
};
windowModeDropdown.Current.BindValueChanged(mode =>
{
if (mode.NewValue == WindowMode.Fullscreen)
{
resolutionDropdown.Show();
sizeFullscreen.TriggerChange();
}
else else
resolutionDropdown.Hide(); windowModeDropdown.Hide();
}, true); }, true);
windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown();
currentDisplay.BindValueChanged(display => Schedule(() =>
{
resolutions.RemoveRange(1, resolutions.Count - 1);
if (display.NewValue != null)
{
resolutions.AddRange(display.NewValue.DisplayModes
.Where(m => m.Size.Width >= 800 && m.Size.Height >= 600)
.OrderByDescending(m => Math.Max(m.Size.Height, m.Size.Width))
.Select(m => m.Size)
.Distinct());
} }
updateResolutionDropdown();
}), true);
scalingSettings.ForEach(s => bindPreviewEvent(s.Current));
scalingMode.BindValueChanged(mode => scalingMode.BindValueChanged(mode =>
{ {
scalingSettings.ClearTransforms(); scalingSettings.ClearTransforms();
@ -163,17 +172,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingSettings.ForEach(s => s.TransferValueOnCommit = mode.NewValue == ScalingMode.Everything); scalingSettings.ForEach(s => s.TransferValueOnCommit = mode.NewValue == ScalingMode.Everything);
}, true); }, true);
windowModes.CollectionChanged += (sender, args) => windowModesChanged(); void updateResolutionDropdown()
windowModesChanged();
}
private void windowModesChanged()
{ {
if (windowModes.Count > 1) if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen)
windowModeDropdown.Show(); resolutionDropdown.Show();
else else
windowModeDropdown.Hide(); resolutionDropdown.Hide();
}
} }
/// <summary> /// <summary>
@ -205,24 +210,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
preview.Expire(); preview.Expire();
} }
private IReadOnlyList<Size> getResolutions()
{
var resolutions = new List<Size> { new Size(9999, 9999) };
var currentDisplay = game.Window?.CurrentDisplayBindable.Value;
if (currentDisplay != null)
{
resolutions.AddRange(currentDisplay.DisplayModes
.Where(m => m.Size.Width >= 800 && m.Size.Height >= 600)
.OrderByDescending(m => m.Size.Width)
.ThenByDescending(m => m.Size.Height)
.Select(m => m.Size)
.Distinct());
}
return resolutions;
}
private class ScalingPreview : ScalingContainer private class ScalingPreview : ScalingContainer
{ {
public ScalingPreview() public ScalingPreview()

View File

@ -1,8 +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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -28,23 +26,20 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
LabelText = "osu! music theme", LabelText = "osu! music theme",
Current = config.GetBindable<bool>(OsuSetting.MenuMusic) Current = config.GetBindable<bool>(OsuSetting.MenuMusic)
}, },
new SettingsDropdown<IntroSequence> new SettingsEnumDropdown<IntroSequence>
{ {
LabelText = "Intro sequence", LabelText = "Intro sequence",
Current = config.GetBindable<IntroSequence>(OsuSetting.IntroSequence), Current = config.GetBindable<IntroSequence>(OsuSetting.IntroSequence),
Items = Enum.GetValues(typeof(IntroSequence)).Cast<IntroSequence>()
}, },
new SettingsDropdown<BackgroundSource> new SettingsEnumDropdown<BackgroundSource>
{ {
LabelText = "Background source", LabelText = "Background source",
Current = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource), Current = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource),
Items = Enum.GetValues(typeof(BackgroundSource)).Cast<BackgroundSource>()
}, },
new SettingsDropdown<SeasonalBackgroundMode> new SettingsEnumDropdown<SeasonalBackgroundMode>
{ {
LabelText = "Seasonal backgrounds", LabelText = "Seasonal backgrounds",
Current = config.GetBindable<SeasonalBackgroundMode>(OsuSetting.SeasonalBackgroundMode), Current = config.GetBindable<SeasonalBackgroundMode>(OsuSetting.SeasonalBackgroundMode),
Items = Enum.GetValues(typeof(SeasonalBackgroundMode)).Cast<SeasonalBackgroundMode>()
} }
}; };
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
@ -25,10 +26,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved] [Resolved]
private EditorBeatmap beatmap { get; set; } private EditorBeatmap beatmap { get; set; }
[Resolved]
private OsuColour colours { get; set; }
private DragEvent lastDragEvent; private DragEvent lastDragEvent;
private Bindable<HitObject> placement; private Bindable<HitObject> placement;
private SelectionBlueprint placementBlueprint; private SelectionBlueprint placementBlueprint;
private readonly Box backgroundBox;
public TimelineBlueprintContainer(HitObjectComposer composer) public TimelineBlueprintContainer(HitObjectComposer composer)
: base(composer) : base(composer)
{ {
@ -36,9 +42,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
Height = 0.4f; Height = 0.6f;
AddInternal(new Box AddInternal(backgroundBox = new Box
{ {
Colour = Color4.Black, Colour = Color4.Black,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -77,6 +83,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; protected override Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };
protected override bool OnHover(HoverEvent e)
{
backgroundBox.FadeColour(colours.BlueLighter, 120, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
backgroundBox.FadeColour(Color4.Black, 600, Easing.OutQuint);
base.OnHoverLost(e);
}
protected override void OnDrag(DragEvent e) protected override void OnDrag(DragEvent e)
{ {
handleScrollViaDrag(e); handleScrollViaDrag(e);

View File

@ -29,7 +29,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
private const float thickness = 5; private const float thickness = 5;
private const float shadow_radius = 5; private const float shadow_radius = 5;
private const float circle_size = 24; private const float circle_size = 34;
public Action<DragEvent> OnDragHandled; public Action<DragEvent> OnDragHandled;

View File

@ -42,8 +42,8 @@ namespace osu.Game.Screens.Menu
public Action OnBeatmapListing; public Action OnBeatmapListing;
public Action OnSolo; public Action OnSolo;
public Action OnSettings; public Action OnSettings;
public Action OnMulti; public Action OnMultiplayer;
public Action OnChart; public Action OnTimeshift;
public const float BUTTON_WIDTH = 140f; public const float BUTTON_WIDTH = 140f;
public const float WEDGE_WIDTH = 20; public const float WEDGE_WIDTH = 20;
@ -124,8 +124,8 @@ namespace osu.Game.Screens.Menu
private void load(AudioManager audio, IdleTracker idleTracker, GameHost host) 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(@"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(@"multi", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 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(@"timeshift", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onTimeshift, 0, Key.L));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); 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)); 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"); sampleBack = audio.Samples.Get(@"Menu/button-back-select");
} }
private void onMulti() private void onMultiplayer()
{ {
if (!api.IsLoggedIn) if (!api.IsLoggedIn)
{ {
@ -172,7 +172,28 @@ namespace osu.Game.Screens.Menu
return; 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) private void updateIdleState(bool isIdle)

View File

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

View File

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

View File

@ -242,7 +242,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components
{ {
new OsuMenuItem("Create copy", MenuItemType.Standard, () => 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 namespace osu.Game.Screens.Multi.Lounge
{ {
[Cached] [Cached]
public class LoungeSubScreen : MultiplayerSubScreen public abstract class LoungeSubScreen : MultiplayerSubScreen
{ {
public override string Title => "Lounge"; public override string Title => "Lounge";
protected FilterControl Filter;
protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby();
private readonly IBindable<bool> initialRoomsReceived = new Bindable<bool>(); private readonly IBindable<bool> initialRoomsReceived = new Bindable<bool>();
private FilterControl filter;
private Container content; private Container content;
private LoadingLayer loadingLayer; private LoadingLayer loadingLayer;
@ -78,11 +77,11 @@ namespace osu.Game.Screens.Multi.Lounge
}, },
}, },
}, },
Filter = new TimeshiftFilterControl filter = CreateFilterControl().With(d =>
{ {
RelativeSizeAxes = Axes.X, d.RelativeSizeAxes = Axes.X;
Height = 80, d.Height = 80;
}, })
}; };
// scroll selected room into view on selection. // scroll selected room into view on selection.
@ -108,7 +107,7 @@ namespace osu.Game.Screens.Multi.Lounge
content.Padding = new MarginPadding content.Padding = new MarginPadding
{ {
Top = Filter.DrawHeight, Top = filter.DrawHeight,
Left = WaveOverlayContainer.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING, Left = WaveOverlayContainer.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING,
Right = WaveOverlayContainer.WIDTH_PADDING + 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) protected override void OnFocus(FocusEvent e)
{ {
Filter.TakeFocus(); filter.TakeFocus();
} }
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
@ -140,19 +139,19 @@ namespace osu.Game.Screens.Multi.Lounge
private void onReturning() private void onReturning()
{ {
Filter.HoldFocus = true; filter.HoldFocus = true;
} }
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
{ {
Filter.HoldFocus = false; filter.HoldFocus = false;
return base.OnExiting(next); return base.OnExiting(next);
} }
public override void OnSuspending(IScreen next) public override void OnSuspending(IScreen next)
{ {
base.OnSuspending(next); base.OnSuspending(next);
Filter.HoldFocus = false; filter.HoldFocus = false;
} }
private void joinRequested(Room room) private void joinRequested(Room room)
@ -193,7 +192,11 @@ namespace osu.Game.Screens.Multi.Lounge
selectedRoom.Value = room; 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. // 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 System;
using System.Collections.Specialized;
using Humanizer;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
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.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Screens.Multi.Match.Components namespace osu.Game.Screens.Multi.Match.Components
{ {
public class MatchSettingsOverlay : FocusedOverlayContainer public abstract class MatchSettingsOverlay : FocusedOverlayContainer
{ {
private const float transition_duration = 350; protected const float TRANSITION_DURATION = 350;
private const float field_padding = 45; protected const float FIELD_PADDING = 45;
public Action EditPlaylist; protected MultiplayerComposite Settings { get; set; }
protected MatchSettings Settings { get; private set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Masking = true; Masking = true;
Child = Settings = new MatchSettings
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y,
EditPlaylist = () => EditPlaylist?.Invoke()
};
} }
protected override void PopIn() protected override void PopIn()
{ {
Settings.MoveToY(0, transition_duration, Easing.OutQuint); Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint);
} }
protected override void PopOut() protected override void PopOut()
{ {
Settings.MoveToY(-1, transition_duration, Easing.InSine); Settings.MoveToY(-1, TRANSITION_DURATION, Easing.InSine);
} }
protected class MatchSettings : MultiplayerComposite protected class SettingsTextBox : OsuTextBox
{
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
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() 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); protected override bool CanAddCharacter(char character) => char.IsNumber(character);
} }
private class SettingsPasswordTextBox : OsuPasswordTextBox protected class SettingsPasswordTextBox : OsuPasswordTextBox
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() 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() public SectionContainer()
{ {
@ -412,11 +68,11 @@ namespace osu.Game.Screens.Multi.Match.Components
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Width = 0.5f; Width = 0.5f;
Direction = FillDirection.Vertical; 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; 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, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Action = () => CreateRoom() Action = () => OpenNewRoom()
}, },
RoomManager = CreateRoomManager() RoomManager = CreateRoomManager()
} }
@ -143,7 +143,7 @@ namespace osu.Game.Screens.Multi
screenStack.ScreenPushed += screenPushed; screenStack.ScreenPushed += screenPushed;
screenStack.ScreenExited += screenExited; screenStack.ScreenExited += screenExited;
screenStack.Push(loungeSubScreen = new LoungeSubScreen()); screenStack.Push(loungeSubScreen = CreateLounge());
} }
private readonly IBindable<APIState> apiState = new Bindable<APIState>(); private readonly IBindable<APIState> apiState = new Bindable<APIState>();
@ -264,10 +264,16 @@ namespace osu.Game.Screens.Multi
} }
/// <summary> /// <summary>
/// Create a new room. /// Creates and opens the newly-created room.
/// </summary> /// </summary>
/// <param name="room">An optional template to use when creating the room.</param> /// <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() private void beginHandlingTrack()
{ {
@ -302,7 +308,7 @@ namespace osu.Game.Screens.Multi
headerBackground.MoveToX(0, MultiplayerSubScreen.X_MOVE_DURATION, Easing.OutQuint); headerBackground.MoveToX(0, MultiplayerSubScreen.X_MOVE_DURATION, Easing.OutQuint);
break; break;
case MatchSubScreen _: case RoomSubScreen _:
header.ResizeHeightTo(135, MultiplayerSubScreen.APPEAR_DURATION, Easing.OutQuint); header.ResizeHeightTo(135, MultiplayerSubScreen.APPEAR_DURATION, Easing.OutQuint);
headerBackground.MoveToX(-MultiplayerSubScreen.X_SHIFT, MultiplayerSubScreen.X_MOVE_DURATION, Easing.OutQuint); headerBackground.MoveToX(-MultiplayerSubScreen.X_SHIFT, MultiplayerSubScreen.X_MOVE_DURATION, Easing.OutQuint);
break; break;
@ -324,7 +330,7 @@ namespace osu.Game.Screens.Multi
private void updateTrack(ValueChangedEvent<WorkingBeatmap> _ = null) private void updateTrack(ValueChangedEvent<WorkingBeatmap> _ = null)
{ {
if (screenStack.CurrentScreen is MatchSubScreen) if (screenStack.CurrentScreen is RoomSubScreen)
{ {
var track = Beatmap.Value?.Track; var track = Beatmap.Value?.Track;
@ -355,6 +361,8 @@ namespace osu.Game.Screens.Multi
protected abstract RoomManager CreateRoomManager(); protected abstract RoomManager CreateRoomManager();
protected abstract LoungeSubScreen CreateLounge();
private class MultiplayerWaveContainer : WaveContainer private class MultiplayerWaveContainer : WaveContainer
{ {
protected override bool StartHidden => true; protected override bool StartHidden => true;

View File

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

View File

@ -32,8 +32,8 @@ namespace osu.Game.Screens.Multi.Ranking
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry) public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true)
: base(score, allowRetry) : base(score, allowRetry, allowWatchingReplay)
{ {
this.roomId = roomId; this.roomId = roomId;
this.playlistItem = playlistItem; 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 osu.Game.Screens.Multi.Components;
using osuTK; using osuTK;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match
{ {
public class RealtimeReadyButton : RealtimeRoomComposite 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) 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) 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() 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); Debug.Assert(room.RoomID.Value != null);
var joinTask = multiplayerClient.JoinRoom(room); var joinTask = multiplayerClient.JoinRoom(room);
joinTask.ContinueWith(_ => onSuccess?.Invoke(room), TaskContinuationOptions.OnlyOnRanToCompletion); joinTask.ContinueWith(_ => Schedule(() => onSuccess?.Invoke(room)), TaskContinuationOptions.OnlyOnRanToCompletion);
joinTask.ContinueWith(t => joinTask.ContinueWith(t =>
{ {
PartRoom(); PartRoom();
if (t.Exception != null) if (t.Exception != null)
Logger.Error(t.Exception, "Failed to join multiplayer room."); Logger.Error(t.Exception, "Failed to join multiplayer room.");
Schedule(() => onError?.Invoke(t.Exception?.ToString() ?? string.Empty));
}, TaskContinuationOptions.NotOnRanToCompletion); }, 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; timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000;
break; break;
case MatchSubScreen _: case RoomSubScreen _:
timeshiftManager.TimeBetweenListingPolls.Value = 0; timeshiftManager.TimeBetweenListingPolls.Value = 0;
timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 30000 : 5000; timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 30000 : 5000;
break; break;
@ -45,5 +45,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();
} }
} }

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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -9,13 +8,10 @@ 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;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Multiplayer; 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.Components;
using osu.Game.Screens.Multi.Match;
using osu.Game.Screens.Multi.Match.Components; using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Screens.Multi.Play; using osu.Game.Screens.Multi.Play;
using osu.Game.Screens.Multi.Ranking; using osu.Game.Screens.Multi.Ranking;
@ -24,13 +20,10 @@ using osu.Game.Screens.Select;
using osu.Game.Users; using osu.Game.Users;
using Footer = osu.Game.Screens.Multi.Match.Components.Footer; 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 TimeshiftRoomSubScreen : RoomSubScreen
public class MatchSubScreen : MultiplayerSubScreen, IPreviewTrackOwner
{ {
public override bool DisallowExternalBeatmapRulesetChanges => true;
public override string Title { get; } public override string Title { get; }
public override string ShortTitle => "room"; public override string ShortTitle => "room";
@ -38,27 +31,15 @@ namespace osu.Game.Screens.Multi.Match
[Resolved(typeof(Room), nameof(Room.RoomID))] [Resolved(typeof(Room), nameof(Room.RoomID))]
private Bindable<int?> roomId { get; set; } 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)] [Resolved(canBeNull: true)]
private Multiplayer multiplayer { get; set; } private Multiplayer multiplayer { get; set; }
protected readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
private MatchSettingsOverlay settingsOverlay; private MatchSettingsOverlay settingsOverlay;
private MatchLeaderboard leaderboard; private MatchLeaderboard leaderboard;
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
private OverlinedHeader participantsHeader; private OverlinedHeader participantsHeader;
public MatchSubScreen(Room room) public TimeshiftRoomSubScreen(Room room)
{ {
Title = room.RoomID.Value == null ? "New room" : room.Name.Value; Title = room.RoomID.Value == null ? "New room" : room.Name.Value;
Activity.Value = new UserActivity.InLobby(room); Activity.Value = new UserActivity.InLobby(room);
@ -96,7 +77,7 @@ namespace osu.Game.Screens.Multi.Match
}, },
Content = new[] Content = new[]
{ {
new Drawable[] { new Components.Header() }, new Drawable[] { new Match.Components.Header() },
new Drawable[] new Drawable[]
{ {
participantsHeader = new OverlinedHeader("Participants") participantsHeader = new OverlinedHeader("Participants")
@ -141,7 +122,7 @@ namespace osu.Game.Screens.Multi.Match
new DrawableRoomPlaylistWithResults new DrawableRoomPlaylistWithResults
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Items = { BindTarget = playlist }, Items = { BindTarget = Playlist },
SelectedItem = { BindTarget = SelectedItem }, SelectedItem = { BindTarget = SelectedItem },
RequestShowResults = item => RequestShowResults = item =>
{ {
@ -208,7 +189,7 @@ namespace osu.Game.Screens.Multi.Match
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
} }
}, },
settingsOverlay = new MatchSettingsOverlay settingsOverlay = new TimeshiftMatchSettingsOverlay
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
EditPlaylist = () => this.Push(new MatchSongSelect()), EditPlaylist = () => this.Push(new MatchSongSelect()),
@ -234,61 +215,17 @@ namespace osu.Game.Screens.Multi.Match
// Set the first playlist item. // Set the first playlist item.
// This is scheduled since updating the room and playlist may happen in an arbitrary order (via Room.CopyFrom()). // 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); }, 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() private void onStart()
{ {
switch (type.Value)
{
default:
case GameTypeTimeshift _:
multiplayer?.Push(new PlayerLoader(() => new TimeshiftPlayer(SelectedItem.Value) multiplayer?.Push(new PlayerLoader(() => new TimeshiftPlayer(SelectedItem.Value)
{ {
Exited = () => leaderboard.RefreshScores() Exited = () => leaderboard.RefreshScores()
})); }));
break;
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1214.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.1222.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="Sentry" Version="2.1.8" /> <PackageReference Include="Sentry" Version="2.1.8" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1214.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1222.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -88,7 +88,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1214.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.1222.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />