diff --git a/osu.Android.props b/osu.Android.props index 591db6e2c2..f8cd4e41d4 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index a535f8c3c3..cd3c50cf14 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -88,6 +89,28 @@ namespace osu.Game.Tests.Visual.Multiplayer // used to test the flow of multiplayer from visual tests. } + [Test] + public void TestCreateRoomViaKeyboard() + { + // create room dialog + AddStep("Press new document", () => InputManager.Keys(PlatformAction.DocumentNew)); + AddUntilStep("wait for settings", () => InputManager.ChildrenOfType().FirstOrDefault() != null); + + // edit playlist item + AddStep("Press select", () => InputManager.Key(Key.Enter)); + AddUntilStep("wait for song select", () => InputManager.ChildrenOfType().FirstOrDefault() != null); + + // select beatmap + AddStep("Press select", () => InputManager.Key(Key.Enter)); + AddUntilStep("wait for return to screen", () => InputManager.ChildrenOfType().FirstOrDefault() == null); + + // create room + AddStep("Press select", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddUntilStep("wait for join", () => client.Room != null); + } + [Test] public void TestCreateRoomWithoutPassword() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs new file mode 100644 index 0000000000..e19665497d --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -0,0 +1,191 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; +using osu.Game.Tests.Resources; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneTeamVersus : ScreenTestScene + { + private BeatmapManager beatmaps; + private RulesetStore rulesets; + private BeatmapSetInfo importedSet; + + private DependenciesScreen dependenciesScreen; + private TestMultiplayer multiplayerScreen; + private TestMultiplayerClient client; + + [Cached(typeof(UserLookupCache))] + private UserLookupCache lookupCache = new TestUserLookupCache(); + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + } + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("import beatmap", () => + { + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + }); + + AddStep("create multiplayer screen", () => multiplayerScreen = new TestMultiplayer()); + + AddStep("load dependencies", () => + { + client = new TestMultiplayerClient(multiplayerScreen.RoomManager); + + // The screen gets suspended so it stops receiving updates. + Child = client; + + LoadScreen(dependenciesScreen = new DependenciesScreen(client)); + }); + + AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded); + + AddStep("load multiplayer", () => LoadScreen(multiplayerScreen)); + AddUntilStep("wait for multiplayer to load", () => multiplayerScreen.IsLoaded); + AddUntilStep("wait for lounge to load", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + } + + [Test] + public void TestCreateWithType() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + Type = { Value = MatchType.TeamVersus }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + + AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus); + AddAssert("user state arrived", () => client.Room?.Users.FirstOrDefault()?.MatchState is TeamVersusUserState); + } + + [Test] + public void TestChangeTeamsViaButton() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + Type = { Value = MatchType.TeamVersus }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + + AddAssert("user on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); + + AddStep("press button", () => + { + InputManager.MoveMouseTo(multiplayerScreen.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("user on team 1", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1); + + AddStep("press button", () => InputManager.Click(MouseButton.Left)); + AddAssert("user on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); + } + + [Test] + public void TestChangeTypeViaMatchSettings() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + + AddAssert("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead); + + AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus)); + + AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus); + } + + private void createRoom(Func room) + { + AddStep("open room", () => + { + multiplayerScreen.OpenNewRoom(room()); + }); + + AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddWaitStep("wait for transition", 2); + + AddStep("create room", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for join", () => client.Room != null); + } + + /// + /// Used for the sole purpose of adding as a resolvable dependency. + /// + private class DependenciesScreen : OsuScreen + { + [Cached(typeof(MultiplayerClient))] + public readonly TestMultiplayerClient Client; + + public DependenciesScreen(TestMultiplayerClient client) + { + Client = client; + } + } + + private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer + { + public new TestRequestHandlingMultiplayerRoomManager RoomManager { get; private set; } + + protected override RoomManager CreateRoomManager() => RoomManager = new TestRequestHandlingMultiplayerRoomManager(); + } + } +} diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index d88fd1e62b..dab5fcbe5f 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -7,6 +7,7 @@ using System.IO; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Audio.Mixing; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -30,11 +31,11 @@ namespace osu.Game.Audio private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(OsuGameBase.GLOBAL_TRACK_VOLUME_ADJUST); [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audioManager) { // this is a temporary solution to get around muting ourselves. // todo: update this once we have a BackgroundTrackManager or similar. - trackStore = new PreviewTrackStore(new OnlineStore()); + trackStore = new PreviewTrackStore(audioManager.Mixer, new OnlineStore()); audio.AddItem(trackStore); trackStore.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust); @@ -118,10 +119,12 @@ namespace osu.Game.Audio private class PreviewTrackStore : AudioCollectionManager, ITrackStore { + private readonly AudioMixer defaultMixer; private readonly IResourceStore store; - internal PreviewTrackStore(IResourceStore store) + internal PreviewTrackStore(AudioMixer defaultMixer, IResourceStore store) { + this.defaultMixer = defaultMixer; this.store = store; } @@ -145,8 +148,12 @@ namespace osu.Game.Audio if (dataStream == null) return null; + // Todo: This is quite unsafe. TrackBass shouldn't be exposed as public. Track track = new TrackBass(dataStream); + + defaultMixer.Add(track); AddItem(track); + return track; } diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 9a3c75dcc6..3210ef0112 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -93,15 +93,15 @@ namespace osu.Game.Beatmaps.Drawables new CircularContainer { RelativeSizeAxes = Axes.Both, - Scale = new Vector2(0.84f), Anchor = Anchor.Centre, Origin = Anchor.Centre, Masking = true, EdgeEffect = new EdgeEffectParameters { - Colour = Color4.Black.Opacity(0.08f), + Colour = Color4.Black.Opacity(0.06f), + Type = EdgeEffectType.Shadow, - Radius = 5, + Radius = 3, }, Child = background = new Box { diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs index 06056f239b..30e38e8938 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs @@ -89,8 +89,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 { SelectionArea.CornerRadius = corner_radius; SelectionArea.Masking = true; - // purposefully use hard non-AA'd masking to avoid edge artifacts. - SelectionArea.MaskingSmoothness = 0; } protected override Marker CreateMarker() => new OsuMarker(); diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 873be7f49c..bffb2d341a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -142,6 +142,8 @@ namespace osu.Game.Online.Multiplayer APIRoom = room; foreach (var user in joinedRoom.Users) updateUserPlayingState(user.UserID, user.State); + + OnRoomJoined(); }, cancellationSource.Token).ConfigureAwait(false); // Update room settings. @@ -149,6 +151,13 @@ namespace osu.Game.Online.Multiplayer }, cancellationSource.Token).ConfigureAwait(false); } + /// + /// Fired when the room join sequence is complete + /// + protected virtual void OnRoomJoined() + { + } + /// /// Joins the with a given ID. /// @@ -192,8 +201,9 @@ namespace osu.Game.Online.Multiplayer /// /// The new room name, if any. /// The new password, if any. + /// The type of the match, if any. /// The new room playlist item, if any. - public Task ChangeSettings(Optional name = default, Optional password = default, Optional item = default) + public Task ChangeSettings(Optional name = default, Optional password = default, Optional matchType = default, Optional item = default) { if (Room == null) throw new InvalidOperationException("Must be joined to a match to change settings."); @@ -219,6 +229,7 @@ namespace osu.Game.Online.Multiplayer BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID, BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, RulesetID = item.GetOr(existingPlaylistItem).RulesetID, + MatchType = matchType.GetOr(Room.Settings.MatchType), RequiredMods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.RequiredMods, AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods, }); diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index 706bc750d3..001cf2aa93 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -41,7 +41,7 @@ namespace osu.Game.Online.Multiplayer public string Password { get; set; } = string.Empty; [Key(8)] - public MatchType MatchType { get; set; } + public MatchType MatchType { get; set; } = MatchType.HeadToHead; public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other.BeatmapID diff --git a/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs b/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs index bbc407e926..2b596da361 100644 --- a/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs +++ b/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs @@ -9,7 +9,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { public abstract class DisableableTabControl : TabControl { - public readonly BindableBool Enabled = new BindableBool(); + public readonly BindableBool Enabled = new BindableBool(true); protected override void AddTabItem(TabItem tab, bool addToDropdown = true) { diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs b/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs new file mode 100644 index 0000000000..cd4dee5e3a --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; + +namespace osu.Game.Screens.OnlinePlay.Match.Components +{ + public abstract class CreateRoomButton : PurpleTriangleButton, IKeyBindingHandler + { + [BackgroundDependencyLoader] + private void load() + { + Triangles.TriangleScale = 1.5f; + } + + public bool OnPressed(PlatformAction action) + { + if (!Enabled.Value) + return false; + + switch (action) + { + case PlatformAction.DocumentNew: + // might as well also handle new tab. it's a bit of an undefined flow on this screen. + case PlatformAction.TabNew: + TriggerClick(); + return true; + } + + return false; + } + + public void OnReleased(PlatformAction action) + { + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs index 61bb39d0c5..2676453a7e 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs @@ -4,15 +4,17 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Match.Components { - public abstract class MatchSettingsOverlay : FocusedOverlayContainer + public abstract class MatchSettingsOverlay : FocusedOverlayContainer, IKeyBindingHandler { protected const float TRANSITION_DURATION = 350; protected const float FIELD_PADDING = 45; @@ -21,6 +23,10 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components protected override bool BlockScrollInput => false; + protected abstract OsuButton SubmitButton { get; } + + protected abstract bool IsLoading { get; } + [BackgroundDependencyLoader] private void load() { @@ -29,6 +35,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components Add(Settings = CreateSettings()); } + protected abstract void SelectBeatmap(); + protected abstract OnlinePlayComposite CreateSettings(); protected override void PopIn() @@ -41,6 +49,33 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components Settings.MoveToY(-1, TRANSITION_DURATION, Easing.InSine); } + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.Select: + if (IsLoading) + return true; + + if (SubmitButton.Enabled.Value) + { + SubmitButton.TriggerClick(); + return true; + } + else + { + SelectBeatmap(); + return true; + } + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } + protected class SettingsTextBox : OsuTextBox { [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchTypePicker.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchTypePicker.cs index c72fa24b67..c6f9b0f207 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchTypePicker.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchTypePicker.cs @@ -30,8 +30,6 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components AddItem(MatchType.HeadToHead); AddItem(MatchType.TeamVersus); - // TODO: remove after osu-web is updated to set the correct default type. - AddItem(MatchType.Playlists); } private class GameTypePickerItem : DisableableTabItem diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs index cc51b5b691..e80923ed47 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs @@ -8,7 +8,7 @@ using osu.Game.Screens.OnlinePlay.Match.Components; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public class CreateMultiplayerMatchButton : PurpleTriangleButton + public class CreateMultiplayerMatchButton : CreateRoomButton { private IBindable isConnected; private IBindable operationInProgress; @@ -22,8 +22,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [BackgroundDependencyLoader] private void load() { - Triangles.TriangleScale = 1.5f; - Text = "Create room"; isConnected = multiplayerClient.IsConnected.GetBoundCopy(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs index ebe63e26d6..56b87302c2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs @@ -47,7 +47,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match RelativeSizeAxes = Axes.X, Height = 40, Text = "Select beatmap", - Action = () => matchSubScreen.Push(new MultiplayerMatchSongSelect()), + Action = () => + { + if (matchSubScreen.IsCurrentScreen()) + matchSubScreen.Push(new MultiplayerMatchSongSelect()); + }, Alpha = 0 } } @@ -68,6 +72,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }, true); } + public void BeginSelection() => selectButton.TriggerClick(); + private void updateBeatmap() { if (SelectedItem.Value == null) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 425252f3cf..5f3921d742 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -28,8 +28,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public class MultiplayerMatchSettingsOverlay : MatchSettingsOverlay { + private MatchSettings settings; + + protected override OsuButton SubmitButton => settings.ApplyButton; + + [Resolved] + private OngoingOperationTracker ongoingOperationTracker { get; set; } + + protected override bool IsLoading => ongoingOperationTracker.InProgress.Value; + + protected override void SelectBeatmap() => settings.SelectBeatmap(); + protected override OnlinePlayComposite CreateSettings() - => new MatchSettings + => settings = new MatchSettings { RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Y, @@ -54,6 +65,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private LoadingLayer loadingLayer; private BeatmapSelectionControl initialBeatmapControl; + public void SelectBeatmap() => initialBeatmapControl.BeginSelection(); + [Resolved] private IRoomManager manager { get; set; } @@ -149,7 +162,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }, new Section("Game type") { - Alpha = disabled_alpha, Child = new FillFlowContainer { AutoSizeAxes = Axes.Y, @@ -161,7 +173,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match TypePicker = new MatchTypePicker { RelativeSizeAxes = Axes.X, - Enabled = { Value = false } }, typeLabel = new OsuSpriteText { @@ -305,7 +316,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match // 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, password: PasswordTextBox.Text).ContinueWith(t => Schedule(() => + client.ChangeSettings(name: NameField.Text, password: PasswordTextBox.Text, matchType: TypePicker.Current.Value).ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) onSuccess(currentRoom.Value); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index dbac826954..d906cc8110 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -58,7 +58,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new Room { Name = { Value = $"{API.LocalUser}'s awesome room" }, - Category = { Value = RoomCategory.Realtime } + Category = { Value = RoomCategory.Realtime }, + Type = { Value = MatchType.HeadToHead }, }; protected override string ScreenTitle => "Multiplayer"; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index f4a334e9d3..89431445d3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -37,6 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants private RulesetStore rulesets { get; set; } private SpriteIcon crown; + private OsuSpriteText userRankText; private ModDisplay userModsDisplay; private StateDisplay userStateDisplay; @@ -56,99 +57,108 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants var backgroundColour = Color4Extensions.FromHex("#33413C"); - InternalChildren = new Drawable[] + InternalChild = new GridContainer { - crown = new SpriteIcon + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Icon = FontAwesome.Solid.Crown, - Size = new Vector2(14), - Colour = Color4Extensions.FromHex("#F7E65D"), - Alpha = 0 + new Dimension(GridSizeMode.Absolute, 18), + new Dimension(GridSizeMode.AutoSize), + new Dimension() }, - new Container + Content = new[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 24 }, - Child = new Container + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 5, - Children = new Drawable[] + crown = new SpriteIcon { - new Box + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.Solid.Crown, + Size = new Vector2(14), + Colour = Color4Extensions.FromHex("#F7E65D"), + Alpha = 0 + }, + new TeamDisplay(user), + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 5, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = backgroundColour - }, - new UserCoverBackground - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Both, - Width = 0.75f, - User = user, - Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.25f)) - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Spacing = new Vector2(10), - Direction = FillDirection.Horizontal, - Children = new Drawable[] + new Box { - new UpdateableAvatar + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour + }, + new UserCoverBackground + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Width = 0.75f, + User = user, + Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.25f)) + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Spacing = new Vector2(10), + Direction = FillDirection.Horizontal, + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit, - User = user - }, - new UpdateableFlag - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(30, 20), - Country = user?.Country - }, - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18), - Text = user?.Username - }, - userRankText = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 14), + new UpdateableAvatar + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + User = user + }, + new UpdateableFlag + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(30, 20), + Country = user?.Country + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18), + Text = user?.Username + }, + userRankText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 14), + } } - } - }, - new Container - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Right = 70 }, - Child = userModsDisplay = new ModDisplay + }, + new Container { - Scale = new Vector2(0.5f), - ExpansionMode = ExpansionMode.AlwaysContracted, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Right = 70 }, + Child = userModsDisplay = new ModDisplay + { + Scale = new Vector2(0.5f), + ExpansionMode = ExpansionMode.AlwaysContracted, + } + }, + userStateDisplay = new StateDisplay + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding { Right = 10 }, } - }, - userStateDisplay = new StateDisplay - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Margin = new MarginPadding { Right = 10 }, } } - } + }, } }; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs new file mode 100644 index 0000000000..5a7073f9de --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs @@ -0,0 +1,134 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; +using osu.Game.Users; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants +{ + internal class TeamDisplay : MultiplayerRoomComposite + { + private readonly User user; + private Drawable box; + + [Resolved] + private OsuColour colours { get; set; } + + [Resolved] + private MultiplayerClient client { get; set; } + + public TeamDisplay(User user) + { + this.user = user; + + RelativeSizeAxes = Axes.Y; + Width = 15; + + Margin = new MarginPadding { Horizontal = 3 }; + + Alpha = 0; + Scale = new Vector2(0, 1); + } + + [BackgroundDependencyLoader] + private void load() + { + box = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 5, + Masking = true, + Scale = new Vector2(0, 1), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + + if (user.Id == client.LocalUser?.UserID) + { + InternalChild = new OsuClickableContainer + { + RelativeSizeAxes = Axes.Both, + TooltipText = "Change team", + Action = changeTeam, + Child = box + }; + } + else + { + InternalChild = box; + } + } + + private void changeTeam() + { + client.SendMatchRequest(new ChangeTeamRequest + { + TeamID = ((client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0, + }); + } + + private int? displayedTeam; + + protected override void OnRoomUpdated() + { + base.OnRoomUpdated(); + + // we don't have a way of knowing when an individual user's state has updated, so just handle on RoomUpdated for now. + + var userRoomState = Room?.Users.FirstOrDefault(u => u.UserID == user.Id)?.MatchState; + + const double duration = 400; + + int? newTeam = (userRoomState as TeamVersusUserState)?.TeamID; + + if (newTeam == displayedTeam) + return; + + displayedTeam = newTeam; + + if (displayedTeam != null) + { + box.FadeColour(getColourForTeam(displayedTeam.Value), duration, Easing.OutQuint); + box.ScaleTo(new Vector2(box.Scale.X < 0 ? 1 : -1, 1), duration, Easing.OutQuint); + + this.ScaleTo(Vector2.One, duration, Easing.OutQuint); + this.FadeIn(duration); + } + else + { + this.ScaleTo(new Vector2(0, 1), duration, Easing.OutQuint); + this.FadeOut(duration); + } + } + + private ColourInfo getColourForTeam(int id) + { + switch (id) + { + default: + return colours.Red; + + case 1: + return colours.Blue; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs index fcb773f8be..a9826a72eb 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs @@ -6,13 +6,11 @@ using osu.Game.Screens.OnlinePlay.Match.Components; namespace osu.Game.Screens.OnlinePlay.Playlists { - public class CreatePlaylistsRoomButton : PurpleTriangleButton + public class CreatePlaylistsRoomButton : CreateRoomButton { [BackgroundDependencyLoader] private void load() { - Triangles.TriangleScale = 1.5f; - Text = "Create playlist"; } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs index 5b132c97fd..4f02651f02 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs @@ -48,7 +48,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override Room CreateNewRoom() { - return new Room { Name = { Value = $"{API.LocalUser}'s awesome playlist" } }; + return new Room + { + Name = { Value = $"{API.LocalUser}'s awesome playlist" }, + Type = { Value = MatchType.Playlists } + }; } protected override string ScreenTitle => "Playlists"; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs index 88ac5ef6e5..2640f99ea5 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs @@ -26,8 +26,16 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { public Action EditPlaylist; + private MatchSettings settings; + + protected override OsuButton SubmitButton => settings.ApplyButton; + + protected override bool IsLoading => settings.IsLoading; // should probably be replaced with an OngoingOperationTracker. + + protected override void SelectBeatmap() => settings.SelectBeatmap(); + protected override OnlinePlayComposite CreateSettings() - => new MatchSettings + => settings = new MatchSettings { RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Y, @@ -45,12 +53,16 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public RoomAvailabilityPicker AvailabilityPicker; public TriangleButton ApplyButton; + public bool IsLoading => loadingLayer.State.Value == Visibility.Visible; + public OsuSpriteText ErrorText; private LoadingLayer loadingLayer; private DrawableRoomPlaylist playlist; private OsuSpriteText playlistLength; + private PurpleTriangleButton editPlaylistButton; + [Resolved(CanBeNull = true)] private IRoomManager manager { get; set; } @@ -199,7 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }, new Drawable[] { - new PurpleTriangleButton + editPlaylistButton = new PurpleTriangleButton { RelativeSizeAxes = Axes.X, Height = 40, @@ -292,6 +304,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists ApplyButton.Enabled.Value = hasValidSettings; } + public void SelectBeatmap() => editPlaylistButton.TriggerClick(); + private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}"; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 092394446b..45aca24ab2 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -230,7 +230,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists settingsOverlay = new PlaylistsMatchSettingsOverlay { RelativeSizeAxes = Axes.Both, - EditPlaylist = () => this.Push(new PlaylistsSongSelect()), + EditPlaylist = () => + { + if (this.IsCurrentScreen()) + this.Push(new PlaylistsSongSelect()); + }, State = { Value = roomId.Value == null ? Visibility.Visible : Visibility.Hidden } } }); diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index ce1f223403..0efa66bac0 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -61,8 +61,6 @@ namespace osu.Game.Screens.Play protected GameplayMenuOverlay() { RelativeSizeAxes = Axes.Both; - - State.ValueChanged += s => InternalButtons.Deselect(); } [BackgroundDependencyLoader] @@ -142,6 +140,8 @@ namespace osu.Game.Screens.Play }, }; + State.ValueChanged += s => InternalButtons.Deselect(); + updateRetryCount(); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 7deecdfa28..43aadf5acb 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -14,6 +14,7 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; using osu.Game.Users; @@ -132,6 +133,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Settings = { Name = apiRoom.Name.Value, + MatchType = apiRoom.Type.Value, BeatmapID = apiRoom.Playlist.Last().BeatmapID, RulesetID = apiRoom.Playlist.Last().RulesetID, BeatmapChecksum = apiRoom.Playlist.Last().Beatmap.Value.MD5Hash, @@ -151,6 +153,14 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.FromResult(room); } + protected override void OnRoomJoined() + { + Debug.Assert(Room != null); + + // emulate the server sending this after the join room. scheduler required to make sure the join room event is fired first (in Join). + changeMatchType(Room.Settings.MatchType).Wait(); + } + protected override Task LeaveRoomInternal() => Task.CompletedTask; public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId); @@ -163,6 +173,8 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) ChangeUserState(user.UserID, MultiplayerUserState.Idle); + + await changeMatchType(settings.MatchType).ConfigureAwait(false); } public override Task ChangeState(MultiplayerUserState newState) @@ -192,7 +204,30 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } - public override Task SendMatchRequest(MatchUserRequest request) => Task.CompletedTask; + public override async Task SendMatchRequest(MatchUserRequest request) + { + Debug.Assert(Room != null); + Debug.Assert(LocalUser != null); + + switch (request) + { + case ChangeTeamRequest changeTeam: + + TeamVersusRoomState roomState = (TeamVersusRoomState)Room.MatchState!; + TeamVersusUserState userState = (TeamVersusUserState)LocalUser.MatchState!; + + var targetTeam = roomState.Teams.FirstOrDefault(t => t.ID == changeTeam.TeamID); + + if (targetTeam != null) + { + userState.TeamID = targetTeam.ID; + + await ((IMultiplayerClient)this).MatchUserStateChanged(LocalUser.UserID, userState).ConfigureAwait(false); + } + + break; + } + } public override Task StartMatch() { @@ -218,5 +253,27 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.FromResult(set); } + + private async Task changeMatchType(MatchType type) + { + Debug.Assert(Room != null); + + switch (type) + { + case MatchType.HeadToHead: + await ((IMultiplayerClient)this).MatchRoomStateChanged(null).ConfigureAwait(false); + + foreach (var user in Room.Users) + await ((IMultiplayerClient)this).MatchUserStateChanged(user.UserID, null).ConfigureAwait(false); + break; + + case MatchType.TeamVersus: + await ((IMultiplayerClient)this).MatchRoomStateChanged(TeamVersusRoomState.CreateDefault()).ConfigureAwait(false); + + foreach (var user in Room.Users) + await ((IMultiplayerClient)this).MatchUserStateChanged(user.UserID, new TeamVersusUserState()).ConfigureAwait(false); + break; + } + } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 0c26bce8c9..ec59b28ceb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index b7f252820c..bcc4b5f254 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - +