1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-14 20:13:22 +08:00

Merge branch 'master' into lounge-redesign

This commit is contained in:
smoogipoo 2021-08-05 19:55:15 +09:00
commit 0246e6f850
22 changed files with 651 additions and 108 deletions

View File

@ -52,7 +52,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.803.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.803.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.804.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.805.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -88,6 +89,28 @@ namespace osu.Game.Tests.Visual.Multiplayer
// used to test the flow of multiplayer from visual tests. // 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<MultiplayerMatchSettingsOverlay>().FirstOrDefault() != null);
// edit playlist item
AddStep("Press select", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for song select", () => InputManager.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault() != null);
// select beatmap
AddStep("Press select", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for return to screen", () => InputManager.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault() == null);
// create room
AddStep("Press select", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => client.Room != null);
}
[Test] [Test]
public void TestCreateRoomWithoutPassword() public void TestCreateRoomWithoutPassword()
{ {

View File

@ -0,0 +1,191 @@
// 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 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<MultiplayerLoungeSubScreen>().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<TeamDisplay>().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> room)
{
AddStep("open room", () =>
{
multiplayerScreen.OpenNewRoom(room());
});
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2);
AddStep("create room", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for join", () => client.Room != null);
}
/// <summary>
/// Used for the sole purpose of adding <see cref="TestMultiplayerClient"/> as a resolvable dependency.
/// </summary>
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();
}
}
}

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Mixing;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -30,11 +31,11 @@ namespace osu.Game.Audio
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(OsuGameBase.GLOBAL_TRACK_VOLUME_ADJUST); private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(OsuGameBase.GLOBAL_TRACK_VOLUME_ADJUST);
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(AudioManager audioManager)
{ {
// this is a temporary solution to get around muting ourselves. // this is a temporary solution to get around muting ourselves.
// todo: update this once we have a BackgroundTrackManager or similar. // 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); audio.AddItem(trackStore);
trackStore.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust); trackStore.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust);
@ -118,10 +119,12 @@ namespace osu.Game.Audio
private class PreviewTrackStore : AudioCollectionManager<AdjustableAudioComponent>, ITrackStore private class PreviewTrackStore : AudioCollectionManager<AdjustableAudioComponent>, ITrackStore
{ {
private readonly AudioMixer defaultMixer;
private readonly IResourceStore<byte[]> store; private readonly IResourceStore<byte[]> store;
internal PreviewTrackStore(IResourceStore<byte[]> store) internal PreviewTrackStore(AudioMixer defaultMixer, IResourceStore<byte[]> store)
{ {
this.defaultMixer = defaultMixer;
this.store = store; this.store = store;
} }
@ -145,8 +148,12 @@ namespace osu.Game.Audio
if (dataStream == null) if (dataStream == null)
return null; return null;
// Todo: This is quite unsafe. TrackBass shouldn't be exposed as public.
Track track = new TrackBass(dataStream); Track track = new TrackBass(dataStream);
defaultMixer.Add(track);
AddItem(track); AddItem(track);
return track; return track;
} }

View File

@ -142,6 +142,8 @@ namespace osu.Game.Online.Multiplayer
APIRoom = room; APIRoom = room;
foreach (var user in joinedRoom.Users) foreach (var user in joinedRoom.Users)
updateUserPlayingState(user.UserID, user.State); updateUserPlayingState(user.UserID, user.State);
OnRoomJoined();
}, cancellationSource.Token).ConfigureAwait(false); }, cancellationSource.Token).ConfigureAwait(false);
// Update room settings. // Update room settings.
@ -149,6 +151,13 @@ namespace osu.Game.Online.Multiplayer
}, cancellationSource.Token).ConfigureAwait(false); }, cancellationSource.Token).ConfigureAwait(false);
} }
/// <summary>
/// Fired when the room join sequence is complete
/// </summary>
protected virtual void OnRoomJoined()
{
}
/// <summary> /// <summary>
/// Joins the <see cref="MultiplayerRoom"/> with a given ID. /// Joins the <see cref="MultiplayerRoom"/> with a given ID.
/// </summary> /// </summary>
@ -192,8 +201,9 @@ namespace osu.Game.Online.Multiplayer
/// </remarks> /// </remarks>
/// <param name="name">The new room name, if any.</param> /// <param name="name">The new room name, if any.</param>
/// <param name="password">The new password, if any.</param> /// <param name="password">The new password, if any.</param>
/// <param name="matchType">The type of the match, 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 Task ChangeSettings(Optional<string> name = default, Optional<string> password = default, Optional<PlaylistItem> item = default) public Task ChangeSettings(Optional<string> name = default, Optional<string> password = default, Optional<MatchType> matchType = default, Optional<PlaylistItem> item = default)
{ {
if (Room == null) if (Room == null)
throw new InvalidOperationException("Must be joined to a match to change settings."); 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, BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID,
BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash,
RulesetID = item.GetOr(existingPlaylistItem).RulesetID, 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, 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, AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods,
}); });

View File

@ -9,7 +9,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
{ {
public abstract class DisableableTabControl<T> : TabControl<T> public abstract class DisableableTabControl<T> : TabControl<T>
{ {
public readonly BindableBool Enabled = new BindableBool(); public readonly BindableBool Enabled = new BindableBool(true);
protected override void AddTabItem(TabItem<T> tab, bool addToDropdown = true) protected override void AddTabItem(TabItem<T> tab, bool addToDropdown = true)
{ {

View File

@ -0,0 +1,39 @@
// 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.Input;
using osu.Framework.Input.Bindings;
namespace osu.Game.Screens.OnlinePlay.Match.Components
{
public abstract class CreateRoomButton : PurpleTriangleButton, IKeyBindingHandler<PlatformAction>
{
[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)
{
}
}
}

View File

@ -4,15 +4,17 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Match.Components namespace osu.Game.Screens.OnlinePlay.Match.Components
{ {
public abstract class MatchSettingsOverlay : FocusedOverlayContainer public abstract class MatchSettingsOverlay : FocusedOverlayContainer, IKeyBindingHandler<GlobalAction>
{ {
protected const float TRANSITION_DURATION = 350; protected const float TRANSITION_DURATION = 350;
protected const float FIELD_PADDING = 45; protected const float FIELD_PADDING = 45;
@ -21,6 +23,10 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
protected override bool BlockScrollInput => false; protected override bool BlockScrollInput => false;
protected abstract OsuButton SubmitButton { get; }
protected abstract bool IsLoading { get; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -29,6 +35,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
Add(Settings = CreateSettings()); Add(Settings = CreateSettings());
} }
protected abstract void SelectBeatmap();
protected abstract OnlinePlayComposite CreateSettings(); protected abstract OnlinePlayComposite CreateSettings();
protected override void PopIn() protected override void PopIn()
@ -41,6 +49,33 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
Settings.MoveToY(-1, TRANSITION_DURATION, Easing.InSine); 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 protected class SettingsTextBox : OsuTextBox
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -8,7 +8,7 @@ using osu.Game.Screens.OnlinePlay.Match.Components;
namespace osu.Game.Screens.OnlinePlay.Multiplayer namespace osu.Game.Screens.OnlinePlay.Multiplayer
{ {
public class CreateMultiplayerMatchButton : PurpleTriangleButton public class CreateMultiplayerMatchButton : CreateRoomButton
{ {
private IBindable<bool> isConnected; private IBindable<bool> isConnected;
private IBindable<bool> operationInProgress; private IBindable<bool> operationInProgress;
@ -22,9 +22,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Triangles.TriangleScale = 1.5f;
SpriteText.Font = SpriteText.Font.With(size: 14); SpriteText.Font = SpriteText.Font.With(size: 14);
Text = "Create room"; Text = "Create room";
isConnected = multiplayerClient.IsConnected.GetBoundCopy(); isConnected = multiplayerClient.IsConnected.GetBoundCopy();

View File

@ -47,7 +47,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 40, Height = 40,
Text = "Select beatmap", Text = "Select beatmap",
Action = () => matchSubScreen.Push(new MultiplayerMatchSongSelect()), Action = () =>
{
if (matchSubScreen.IsCurrentScreen())
matchSubScreen.Push(new MultiplayerMatchSongSelect());
},
Alpha = 0 Alpha = 0
} }
} }
@ -68,6 +72,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}, true); }, true);
} }
public void BeginSelection() => selectButton.TriggerClick();
private void updateBeatmap() private void updateBeatmap()
{ {
if (SelectedItem.Value == null) if (SelectedItem.Value == null)

View File

@ -28,8 +28,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
public class MultiplayerMatchSettingsOverlay : MatchSettingsOverlay 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() protected override OnlinePlayComposite CreateSettings()
=> new MatchSettings => settings = new MatchSettings
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y, RelativePositionAxes = Axes.Y,
@ -54,6 +65,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private LoadingLayer loadingLayer; private LoadingLayer loadingLayer;
private BeatmapSelectionControl initialBeatmapControl; private BeatmapSelectionControl initialBeatmapControl;
public void SelectBeatmap() => initialBeatmapControl.BeginSelection();
[Resolved] [Resolved]
private IRoomManager manager { get; set; } private IRoomManager manager { get; set; }
@ -149,7 +162,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}, },
new Section("Game type") new Section("Game type")
{ {
Alpha = disabled_alpha,
Child = new FillFlowContainer Child = new FillFlowContainer
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
@ -161,7 +173,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
TypePicker = new MatchTypePicker TypePicker = new MatchTypePicker
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Enabled = { Value = false }
}, },
typeLabel = new OsuSpriteText 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. // Otherwise, update the room directly in preparation for it to be submitted to the API on match creation.
if (client.Room != null) 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) if (t.IsCompletedSuccessfully)
onSuccess(currentRoom.Value); onSuccess(currentRoom.Value);

View File

@ -28,7 +28,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override Room CreateNewRoom() => new Room protected override Room CreateNewRoom() => new Room
{ {
Name = { Value = $"{api.LocalUser}'s awesome room" }, Name = { Value = $"{api.LocalUser}'s awesome room" },
Category = { Value = RoomCategory.Realtime } Category = { Value = RoomCategory.Realtime },
Type = { Value = MatchType.HeadToHead },
}; };
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room); protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room);

View File

@ -37,6 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
private RulesetStore rulesets { get; set; } private RulesetStore rulesets { get; set; }
private SpriteIcon crown; private SpriteIcon crown;
private OsuSpriteText userRankText; private OsuSpriteText userRankText;
private ModDisplay userModsDisplay; private ModDisplay userModsDisplay;
private StateDisplay userStateDisplay; private StateDisplay userStateDisplay;
@ -56,99 +57,108 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
var backgroundColour = Color4Extensions.FromHex("#33413C"); var backgroundColour = Color4Extensions.FromHex("#33413C");
InternalChildren = new Drawable[] InternalChild = new GridContainer
{ {
crown = new SpriteIcon RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{ {
Anchor = Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 18),
Origin = Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize),
Icon = FontAwesome.Solid.Crown, new Dimension()
Size = new Vector2(14),
Colour = Color4Extensions.FromHex("#F7E65D"),
Alpha = 0
}, },
new Container Content = new[]
{ {
RelativeSizeAxes = Axes.Both, new Drawable[]
Padding = new MarginPadding { Left = 24 },
Child = new Container
{ {
RelativeSizeAxes = Axes.Both, crown = new SpriteIcon
Masking = true,
CornerRadius = 5,
Children = new Drawable[]
{ {
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, new Box
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 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, new UpdateableAvatar
Origin = Anchor.CentreLeft, {
RelativeSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft,
FillMode = FillMode.Fit, Origin = Anchor.CentreLeft,
User = user RelativeSizeAxes = Axes.Both,
}, FillMode = FillMode.Fit,
new UpdateableFlag User = user
{ },
Anchor = Anchor.CentreLeft, new UpdateableFlag
Origin = Anchor.CentreLeft, {
Size = new Vector2(30, 20), Anchor = Anchor.CentreLeft,
Country = user?.Country Origin = Anchor.CentreLeft,
}, Size = new Vector2(30, 20),
new OsuSpriteText Country = user?.Country
{ },
Anchor = Anchor.CentreLeft, new OsuSpriteText
Origin = Anchor.CentreLeft, {
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18), Anchor = Anchor.CentreLeft,
Text = user?.Username Origin = Anchor.CentreLeft,
}, Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18),
userRankText = new OsuSpriteText Text = user?.Username
{ },
Anchor = Anchor.CentreLeft, userRankText = new OsuSpriteText
Origin = Anchor.CentreLeft, {
Font = OsuFont.GetFont(size: 14), Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 14),
}
} }
} },
}, new Container
new Container
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Right = 70 },
Child = userModsDisplay = new ModDisplay
{ {
Scale = new Vector2(0.5f), Anchor = Anchor.CentreRight,
ExpansionMode = ExpansionMode.AlwaysContracted, 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 },
} }
} }
} },
} }
}; };
} }

View File

@ -0,0 +1,134 @@
// 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 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;
}
}
}
}

View File

@ -6,14 +6,12 @@ using osu.Game.Screens.OnlinePlay.Match.Components;
namespace osu.Game.Screens.OnlinePlay.Playlists namespace osu.Game.Screens.OnlinePlay.Playlists
{ {
public class CreatePlaylistsRoomButton : PurpleTriangleButton public class CreatePlaylistsRoomButton : CreateRoomButton
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Triangles.TriangleScale = 1.5f;
SpriteText.Font = SpriteText.Font.With(size: 14); SpriteText.Font = SpriteText.Font.With(size: 14);
Text = "Create playlist"; Text = "Create playlist";
} }
} }

View File

@ -22,7 +22,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override Room CreateNewRoom() 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 RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room); protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room);

View File

@ -26,8 +26,16 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{ {
public Action EditPlaylist; 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() protected override OnlinePlayComposite CreateSettings()
=> new MatchSettings => settings = new MatchSettings
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y, RelativePositionAxes = Axes.Y,
@ -45,12 +53,16 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
public RoomAvailabilityPicker AvailabilityPicker; public RoomAvailabilityPicker AvailabilityPicker;
public TriangleButton ApplyButton; public TriangleButton ApplyButton;
public bool IsLoading => loadingLayer.State.Value == Visibility.Visible;
public OsuSpriteText ErrorText; public OsuSpriteText ErrorText;
private LoadingLayer loadingLayer; private LoadingLayer loadingLayer;
private DrawableRoomPlaylist playlist; private DrawableRoomPlaylist playlist;
private OsuSpriteText playlistLength; private OsuSpriteText playlistLength;
private PurpleTriangleButton editPlaylistButton;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IRoomManager manager { get; set; } private IRoomManager manager { get; set; }
@ -199,7 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}, },
new Drawable[] new Drawable[]
{ {
new PurpleTriangleButton editPlaylistButton = new PurpleTriangleButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 40, Height = 40,
@ -292,6 +304,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
ApplyButton.Enabled.Value = hasValidSettings; ApplyButton.Enabled.Value = hasValidSettings;
} }
public void SelectBeatmap() => editPlaylistButton.TriggerClick();
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) =>
playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}"; playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}";

View File

@ -230,7 +230,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
settingsOverlay = new PlaylistsMatchSettingsOverlay settingsOverlay = new PlaylistsMatchSettingsOverlay
{ {
RelativeSizeAxes = Axes.Both, 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 } State = { Value = roomId.Value == null ? Visibility.Visible : Visibility.Hidden }
} }
}); });

View File

@ -61,8 +61,6 @@ namespace osu.Game.Screens.Play
protected GameplayMenuOverlay() protected GameplayMenuOverlay()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
State.ValueChanged += s => InternalButtons.Deselect();
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -142,6 +140,8 @@ namespace osu.Game.Screens.Play
}, },
}; };
State.ValueChanged += s => InternalButtons.Deselect();
updateRetryCount(); updateRetryCount();
} }

View File

@ -14,6 +14,7 @@ using osu.Framework.Bindables;
using osu.Game.Beatmaps; 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.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Users; using osu.Game.Users;
@ -132,6 +133,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Settings = Settings =
{ {
Name = apiRoom.Name.Value, Name = apiRoom.Name.Value,
MatchType = apiRoom.Type.Value,
BeatmapID = apiRoom.Playlist.Last().BeatmapID, BeatmapID = apiRoom.Playlist.Last().BeatmapID,
RulesetID = apiRoom.Playlist.Last().RulesetID, RulesetID = apiRoom.Playlist.Last().RulesetID,
BeatmapChecksum = apiRoom.Playlist.Last().Beatmap.Value.MD5Hash, BeatmapChecksum = apiRoom.Playlist.Last().Beatmap.Value.MD5Hash,
@ -151,6 +153,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
return Task.FromResult(room); 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; protected override Task LeaveRoomInternal() => Task.CompletedTask;
public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId); 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)) foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
ChangeUserState(user.UserID, MultiplayerUserState.Idle); ChangeUserState(user.UserID, MultiplayerUserState.Idle);
await changeMatchType(settings.MatchType).ConfigureAwait(false);
} }
public override Task ChangeState(MultiplayerUserState newState) public override Task ChangeState(MultiplayerUserState newState)
@ -192,7 +204,30 @@ namespace osu.Game.Tests.Visual.Multiplayer
return Task.CompletedTask; 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() public override Task StartMatch()
{ {
@ -218,5 +253,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
return Task.FromResult(set); 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;
}
}
} }
} }

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.3.0" /> <PackageReference Include="Realm" Version="10.3.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.804.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.805.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.803.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.803.0" />
<PackageReference Include="Sentry" Version="3.8.3" /> <PackageReference Include="Sentry" Version="3.8.3" />
<PackageReference Include="SharpCompress" Version="0.28.3" /> <PackageReference Include="SharpCompress" Version="0.28.3" />

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="2021.804.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.805.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.803.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.803.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) -->
@ -93,7 +93,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="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.804.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.805.0" />
<PackageReference Include="SharpCompress" Version="0.28.3" /> <PackageReference Include="SharpCompress" Version="0.28.3" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />