mirror of
https://github.com/ppy/osu.git
synced 2025-01-15 14:12:54 +08:00
Merge branch 'master' into fix-realm-import-test-assert-failure
This commit is contained in:
commit
b76cd2beae
338
osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
Normal file
338
osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
// 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.Bindables;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.Multiplayer.Countdown;
|
||||||
|
using osu.Game.Online.Rooms;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
|
{
|
||||||
|
public class TestSceneMatchStartControl : MultiplayerTestScene
|
||||||
|
{
|
||||||
|
private MatchStartControl control;
|
||||||
|
private BeatmapSetInfo importedSet;
|
||||||
|
|
||||||
|
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
||||||
|
|
||||||
|
private BeatmapManager beatmaps;
|
||||||
|
private RulesetStore rulesets;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(GameHost host, AudioManager audio)
|
||||||
|
{
|
||||||
|
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||||
|
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
|
||||||
|
Dependencies.Cache(Realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public new void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
|
||||||
|
|
||||||
|
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||||
|
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
|
||||||
|
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
|
||||||
|
|
||||||
|
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
||||||
|
{
|
||||||
|
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
|
||||||
|
};
|
||||||
|
|
||||||
|
Child = new PopoverContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = control = new MatchStartControl
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new Vector2(200, 50),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestStartWithCountdown()
|
||||||
|
{
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||||
|
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||||
|
AddStep("click the first countdown button", () =>
|
||||||
|
{
|
||||||
|
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
|
||||||
|
InputManager.MoveMouseTo(popoverButton);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
|
||||||
|
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
|
||||||
|
AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCancelCountdown()
|
||||||
|
{
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||||
|
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||||
|
AddStep("click the first countdown button", () =>
|
||||||
|
{
|
||||||
|
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
|
||||||
|
InputManager.MoveMouseTo(popoverButton);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
|
||||||
|
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
|
||||||
|
AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReadyAndUnReadyDuringCountdown()
|
||||||
|
{
|
||||||
|
AddStep("add second user as host", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||||
|
MultiplayerClient.TransferHost(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(2) }).WaitSafely());
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCountdownButtonEnablementAndVisibilityWhileSpectating()
|
||||||
|
{
|
||||||
|
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
|
||||||
|
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
||||||
|
|
||||||
|
AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
|
||||||
|
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
|
||||||
|
|
||||||
|
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }));
|
||||||
|
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
|
||||||
|
|
||||||
|
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
|
||||||
|
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSpectatingDuringCountdownWithNoReadyUsersCancelsCountdown()
|
||||||
|
{
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||||
|
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||||
|
AddStep("click the first countdown button", () =>
|
||||||
|
{
|
||||||
|
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
|
||||||
|
InputManager.MoveMouseTo(popoverButton);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
|
||||||
|
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
||||||
|
|
||||||
|
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
|
||||||
|
AddUntilStep("match not started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.Open);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReadyButtonEnabledWhileSpectatingDuringCountdown()
|
||||||
|
{
|
||||||
|
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }));
|
||||||
|
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||||
|
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||||
|
AddStep("click the first countdown button", () =>
|
||||||
|
{
|
||||||
|
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
|
||||||
|
InputManager.MoveMouseTo(popoverButton);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
|
||||||
|
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
||||||
|
|
||||||
|
AddAssert("ready button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBecomeHostDuringCountdownAndReady()
|
||||||
|
{
|
||||||
|
AddStep("add second user as host", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||||
|
MultiplayerClient.TransferHost(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely());
|
||||||
|
AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null);
|
||||||
|
|
||||||
|
AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID));
|
||||||
|
AddUntilStep("local user is host", () => MultiplayerClient.Room?.Host?.Equals(MultiplayerClient.LocalUser) == true);
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
|
||||||
|
AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDeletedBeatmapDisableReady()
|
||||||
|
{
|
||||||
|
OsuButton readyButton = null;
|
||||||
|
|
||||||
|
AddUntilStep("ensure ready button enabled", () =>
|
||||||
|
{
|
||||||
|
readyButton = control.ChildrenOfType<OsuButton>().Single();
|
||||||
|
return readyButton.Enabled.Value;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
|
||||||
|
AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);
|
||||||
|
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
|
||||||
|
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestToggleStateWhenNotHost()
|
||||||
|
{
|
||||||
|
AddStep("add second user as host", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||||
|
MultiplayerClient.TransferHost(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(true)]
|
||||||
|
[TestCase(false)]
|
||||||
|
public void TestToggleStateWhenHost(bool allReady)
|
||||||
|
{
|
||||||
|
AddStep("setup", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
|
||||||
|
|
||||||
|
if (!allReady)
|
||||||
|
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||||
|
});
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||||
|
|
||||||
|
verifyGameplayStartFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBecomeHostWhileReady()
|
||||||
|
{
|
||||||
|
AddStep("add host", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||||
|
MultiplayerClient.TransferHost(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
|
||||||
|
|
||||||
|
verifyGameplayStartFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLoseHostWhileReady()
|
||||||
|
{
|
||||||
|
AddStep("setup", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
|
||||||
|
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||||
|
});
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||||
|
|
||||||
|
AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0));
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
||||||
|
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(true)]
|
||||||
|
[TestCase(false)]
|
||||||
|
public void TestManyUsersChangingState(bool isHost)
|
||||||
|
{
|
||||||
|
const int users = 10;
|
||||||
|
AddStep("setup", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
|
||||||
|
for (int i = 0; i < users; i++)
|
||||||
|
MultiplayerClient.AddUser(new APIUser { Id = i, Username = "Another user" });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isHost)
|
||||||
|
AddStep("transfer host", () => MultiplayerClient.TransferHost(2));
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
|
||||||
|
AddRepeatStep("change user ready state", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle);
|
||||||
|
}, 20);
|
||||||
|
|
||||||
|
AddRepeatStep("ready all users", () =>
|
||||||
|
{
|
||||||
|
var nextUnready = MultiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
|
||||||
|
if (nextUnready != null)
|
||||||
|
MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
|
||||||
|
}, users);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyGameplayStartFlow()
|
||||||
|
{
|
||||||
|
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
|
||||||
|
|
||||||
|
AddStep("finish gameplay", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded);
|
||||||
|
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -41,6 +41,7 @@ using osu.Game.Screens.Ranking;
|
|||||||
using osu.Game.Screens.Spectate;
|
using osu.Game.Screens.Spectate;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
using ReadyButton = osu.Game.Screens.OnlinePlay.Components.ReadyButton;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
{
|
{
|
||||||
@ -424,7 +425,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != multiplayerClient.Room?.Playlist.First().BeatmapID);
|
AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != multiplayerClient.Room?.Playlist.First().BeatmapID);
|
||||||
|
|
||||||
AddStep("start match externally", () => multiplayerClient.StartMatch());
|
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
|
||||||
|
|
||||||
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
|
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
|
||||||
|
|
||||||
@ -462,7 +463,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != multiplayerClient.Room?.Playlist.First().RulesetID);
|
AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != multiplayerClient.Room?.Playlist.First().RulesetID);
|
||||||
|
|
||||||
AddStep("start match externally", () => multiplayerClient.StartMatch());
|
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
|
||||||
|
|
||||||
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
|
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
|
||||||
|
|
||||||
@ -500,7 +501,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
|
AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
|
||||||
|
|
||||||
AddStep("start match externally", () => multiplayerClient.StartMatch());
|
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
|
||||||
|
|
||||||
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
|
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
|
||||||
|
|
||||||
@ -535,7 +536,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
||||||
|
|
||||||
AddStep("start match externally", () => multiplayerClient.StartMatch());
|
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
|
||||||
|
|
||||||
AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen());
|
AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen());
|
||||||
}
|
}
|
||||||
@ -568,7 +569,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
||||||
|
|
||||||
AddStep("start match externally", () => multiplayerClient.StartMatch());
|
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
|
||||||
|
|
||||||
AddStep("restore beatmap", () =>
|
AddStep("restore beatmap", () =>
|
||||||
{
|
{
|
||||||
@ -883,7 +884,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddStep("start match by other user", () =>
|
AddStep("start match by other user", () =>
|
||||||
{
|
{
|
||||||
multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready);
|
multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready);
|
||||||
multiplayerClient.StartMatch();
|
multiplayerClient.StartMatch().WaitSafely();
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for loading", () => multiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad);
|
AddUntilStep("wait for loading", () => multiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad);
|
||||||
|
@ -1,199 +0,0 @@
|
|||||||
// 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.Allocation;
|
|
||||||
using osu.Framework.Audio;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Extensions;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Platform;
|
|
||||||
using osu.Framework.Testing;
|
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
|
||||||
using osu.Game.Online.Multiplayer;
|
|
||||||
using osu.Game.Online.Rooms;
|
|
||||||
using osu.Game.Rulesets;
|
|
||||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
|
||||||
using osu.Game.Tests.Resources;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
|
||||||
{
|
|
||||||
public class TestSceneMultiplayerReadyButton : MultiplayerTestScene
|
|
||||||
{
|
|
||||||
private MultiplayerReadyButton button;
|
|
||||||
private BeatmapSetInfo importedSet;
|
|
||||||
|
|
||||||
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
|
||||||
|
|
||||||
private BeatmapManager beatmaps;
|
|
||||||
private RulesetStore rulesets;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(GameHost host, AudioManager audio)
|
|
||||||
{
|
|
||||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
|
||||||
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
|
|
||||||
Dependencies.Cache(Realm);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public new void Setup() => Schedule(() =>
|
|
||||||
{
|
|
||||||
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
|
|
||||||
|
|
||||||
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
|
||||||
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
|
|
||||||
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
|
|
||||||
|
|
||||||
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
|
||||||
{
|
|
||||||
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
|
|
||||||
};
|
|
||||||
|
|
||||||
if (button != null)
|
|
||||||
Remove(button);
|
|
||||||
|
|
||||||
Add(button = new MultiplayerReadyButton
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Size = new Vector2(200, 50),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestDeletedBeatmapDisableReady()
|
|
||||||
{
|
|
||||||
OsuButton readyButton = null;
|
|
||||||
|
|
||||||
AddUntilStep("ensure ready button enabled", () =>
|
|
||||||
{
|
|
||||||
readyButton = button.ChildrenOfType<OsuButton>().Single();
|
|
||||||
return readyButton.Enabled.Value;
|
|
||||||
});
|
|
||||||
|
|
||||||
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
|
|
||||||
AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);
|
|
||||||
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
|
|
||||||
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestToggleStateWhenNotHost()
|
|
||||||
{
|
|
||||||
AddStep("add second user as host", () =>
|
|
||||||
{
|
|
||||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
|
||||||
MultiplayerClient.TransferHost(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
|
||||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
|
||||||
|
|
||||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
|
||||||
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(true)]
|
|
||||||
[TestCase(false)]
|
|
||||||
public void TestToggleStateWhenHost(bool allReady)
|
|
||||||
{
|
|
||||||
AddStep("setup", () =>
|
|
||||||
{
|
|
||||||
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
|
|
||||||
|
|
||||||
if (!allReady)
|
|
||||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
|
||||||
});
|
|
||||||
|
|
||||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
|
||||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
|
||||||
|
|
||||||
verifyGameplayStartFlow();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestBecomeHostWhileReady()
|
|
||||||
{
|
|
||||||
AddStep("add host", () =>
|
|
||||||
{
|
|
||||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
|
||||||
MultiplayerClient.TransferHost(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
|
||||||
AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
|
|
||||||
|
|
||||||
verifyGameplayStartFlow();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestLoseHostWhileReady()
|
|
||||||
{
|
|
||||||
AddStep("setup", () =>
|
|
||||||
{
|
|
||||||
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
|
|
||||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
|
||||||
});
|
|
||||||
|
|
||||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
|
||||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
|
||||||
|
|
||||||
AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0));
|
|
||||||
|
|
||||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
|
||||||
AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
|
||||||
AddUntilStep("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(true)]
|
|
||||||
[TestCase(false)]
|
|
||||||
public void TestManyUsersChangingState(bool isHost)
|
|
||||||
{
|
|
||||||
const int users = 10;
|
|
||||||
AddStep("setup", () =>
|
|
||||||
{
|
|
||||||
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
|
|
||||||
for (int i = 0; i < users; i++)
|
|
||||||
MultiplayerClient.AddUser(new APIUser { Id = i, Username = "Another user" });
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isHost)
|
|
||||||
AddStep("transfer host", () => MultiplayerClient.TransferHost(2));
|
|
||||||
|
|
||||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
|
||||||
|
|
||||||
AddRepeatStep("change user ready state", () =>
|
|
||||||
{
|
|
||||||
MultiplayerClient.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle);
|
|
||||||
}, 20);
|
|
||||||
|
|
||||||
AddRepeatStep("ready all users", () =>
|
|
||||||
{
|
|
||||||
var nextUnready = MultiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
|
|
||||||
if (nextUnready != null)
|
|
||||||
MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
|
|
||||||
}, users);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void verifyGameplayStartFlow()
|
|
||||||
{
|
|
||||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
|
||||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
|
||||||
AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
|
|
||||||
|
|
||||||
AddStep("finish gameplay", () =>
|
|
||||||
{
|
|
||||||
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded);
|
|
||||||
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
|
|
||||||
});
|
|
||||||
|
|
||||||
AddUntilStep("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,6 +9,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -26,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene
|
public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene
|
||||||
{
|
{
|
||||||
private MultiplayerSpectateButton spectateButton;
|
private MultiplayerSpectateButton spectateButton;
|
||||||
private MultiplayerReadyButton readyButton;
|
private MatchStartControl startControl;
|
||||||
|
|
||||||
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
||||||
|
|
||||||
@ -56,6 +57,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID,
|
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Child = new PopoverContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
Child = new FillFlowContainer
|
Child = new FillFlowContainer
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both,
|
AutoSizeAxes = Axes.Both,
|
||||||
@ -68,13 +72,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Size = new Vector2(200, 50),
|
Size = new Vector2(200, 50),
|
||||||
},
|
},
|
||||||
readyButton = new MultiplayerReadyButton
|
startControl = new MatchStartControl
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Size = new Vector2(200, 50),
|
Size = new Vector2(200, 50),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -141,6 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
=> AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
|
=> AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
|
||||||
|
|
||||||
private void assertReadyButtonEnablement(bool shouldBeEnabled)
|
private void assertReadyButtonEnablement(bool shouldBeEnabled)
|
||||||
=> AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => readyButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
|
=> AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => startControl.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value == shouldBeEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
// 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 MessagePack;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Multiplayer.Countdown
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates a change to the <see cref="MultiplayerRoom"/>'s countdown.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class CountdownChangedEvent : MatchServerEvent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The new countdown.
|
||||||
|
/// </summary>
|
||||||
|
[Key(0)]
|
||||||
|
public MultiplayerCountdown? Countdown { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -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 System;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Multiplayer.Countdown
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A request for a countdown to start the match.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class StartMatchCountdownRequest : MatchUserRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// How long the countdown should last.
|
||||||
|
/// </summary>
|
||||||
|
[Key(0)]
|
||||||
|
public TimeSpan Duration { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -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.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Multiplayer.Countdown
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Request to stop the current countdown.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class StopCountdownRequest : MatchUserRequest
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
|
using osu.Game.Online.Multiplayer.Countdown;
|
||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
@ -11,6 +14,8 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Serializable]
|
[Serializable]
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
|
// IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
|
||||||
|
[Union(0, typeof(CountdownChangedEvent))]
|
||||||
public abstract class MatchServerEvent
|
public abstract class MatchServerEvent
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
17
osu.Game/Online/Multiplayer/MatchStartCountdown.cs
Normal file
17
osu.Game/Online/Multiplayer/MatchStartCountdown.cs
Normal 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.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Multiplayer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="MultiplayerCountdown"/> which will start the match after ending.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class MatchStartCountdown : MultiplayerCountdown
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ using MessagePack;
|
|||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
|
namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
|
||||||
{
|
{
|
||||||
|
[MessagePackObject]
|
||||||
public class ChangeTeamRequest : MatchUserRequest
|
public class ChangeTeamRequest : MatchUserRequest
|
||||||
{
|
{
|
||||||
[Key(0)]
|
[Key(0)]
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
|
using osu.Game.Online.Multiplayer.Countdown;
|
||||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer
|
namespace osu.Game.Online.Multiplayer
|
||||||
@ -12,7 +13,10 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Serializable]
|
[Serializable]
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
[Union(0, typeof(ChangeTeamRequest))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
|
// IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
|
||||||
|
[Union(0, typeof(ChangeTeamRequest))]
|
||||||
|
[Union(1, typeof(StartMatchCountdownRequest))]
|
||||||
|
[Union(2, typeof(StopCountdownRequest))]
|
||||||
public abstract class MatchUserRequest
|
public abstract class MatchUserRequest
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ using osu.Framework.Logging;
|
|||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Multiplayer.Countdown;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Online.Rooms.RoomStatuses;
|
using osu.Game.Online.Rooms.RoomStatuses;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
@ -534,7 +535,24 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
|
|
||||||
public Task MatchEvent(MatchServerEvent e)
|
public Task MatchEvent(MatchServerEvent e)
|
||||||
{
|
{
|
||||||
// not used by any match types just yet.
|
if (Room == null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (e)
|
||||||
|
{
|
||||||
|
case CountdownChangedEvent countdownChangedEvent:
|
||||||
|
Room.Countdown = countdownChangedEvent.Countdown;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomUpdated?.Invoke();
|
||||||
|
}, false);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
28
osu.Game/Online/Multiplayer/MultiplayerCountdown.cs
Normal file
28
osu.Game/Online/Multiplayer/MultiplayerCountdown.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// 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 MessagePack;
|
||||||
|
using osu.Game.Online.Multiplayer.Countdown;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Multiplayer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Describes the current countdown in a <see cref="MultiplayerRoom"/>.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
[Union(0, typeof(MatchStartCountdown))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
|
||||||
|
public abstract class MultiplayerCountdown
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The amount of time remaining in the countdown.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is only sent once from the server upon initial retrieval of the <see cref="MultiplayerRoom"/> or via a <see cref="CountdownChangedEvent"/>.
|
||||||
|
/// </remarks>
|
||||||
|
[Key(0)]
|
||||||
|
public TimeSpan TimeRemaining { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -54,6 +54,12 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
[Key(6)]
|
[Key(6)]
|
||||||
public IList<MultiplayerPlaylistItem> Playlist { get; set; } = new List<MultiplayerPlaylistItem>();
|
public IList<MultiplayerPlaylistItem> Playlist { get; set; } = new List<MultiplayerPlaylistItem>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The currently-running countdown.
|
||||||
|
/// </summary>
|
||||||
|
[Key(7)]
|
||||||
|
public MultiplayerCountdown? Countdown { get; set; }
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
[SerializationConstructor]
|
[SerializationConstructor]
|
||||||
public MultiplayerRoom(long roomId)
|
public MultiplayerRoom(long roomId)
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.Multiplayer.Countdown;
|
||||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||||
|
|
||||||
namespace osu.Game.Online
|
namespace osu.Game.Online
|
||||||
@ -18,8 +19,12 @@ namespace osu.Game.Online
|
|||||||
internal static readonly IReadOnlyList<(Type derivedType, Type baseType)> BASE_TYPE_MAPPING = new[]
|
internal static readonly IReadOnlyList<(Type derivedType, Type baseType)> BASE_TYPE_MAPPING = new[]
|
||||||
{
|
{
|
||||||
(typeof(ChangeTeamRequest), typeof(MatchUserRequest)),
|
(typeof(ChangeTeamRequest), typeof(MatchUserRequest)),
|
||||||
|
(typeof(StartMatchCountdownRequest), typeof(MatchUserRequest)),
|
||||||
|
(typeof(StopCountdownRequest), typeof(MatchUserRequest)),
|
||||||
|
(typeof(CountdownChangedEvent), typeof(MatchServerEvent)),
|
||||||
(typeof(TeamVersusRoomState), typeof(MatchRoomState)),
|
(typeof(TeamVersusRoomState), typeof(MatchRoomState)),
|
||||||
(typeof(TeamVersusUserState), typeof(MatchUserState)),
|
(typeof(TeamVersusUserState), typeof(MatchUserState)),
|
||||||
|
(typeof(MatchStartCountdown), typeof(MultiplayerCountdown))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,12 +15,12 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
|||||||
{
|
{
|
||||||
public new readonly BindableBool Enabled = new BindableBool();
|
public new readonly BindableBool Enabled = new BindableBool();
|
||||||
|
|
||||||
private IBindable<BeatmapAvailability> availability;
|
private readonly IBindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>();
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OnlinePlayBeatmapAvailabilityTracker beatmapTracker)
|
private void load(OnlinePlayBeatmapAvailabilityTracker beatmapTracker)
|
||||||
{
|
{
|
||||||
availability = beatmapTracker.Availability.GetBoundCopy();
|
availability.BindTo(beatmapTracker.Availability);
|
||||||
|
|
||||||
availability.BindValueChanged(_ => updateState());
|
availability.BindValueChanged(_ => updateState());
|
||||||
Enabled.BindValueChanged(_ => updateState(), true);
|
Enabled.BindValueChanged(_ => updateState(), true);
|
||||||
|
@ -12,6 +12,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Extensions.Color4Extensions;
|
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.Cursor;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
@ -100,7 +101,10 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
|||||||
{
|
{
|
||||||
sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection");
|
sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection");
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChild = new PopoverContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
beatmapAvailabilityTracker,
|
beatmapAvailabilityTracker,
|
||||||
new MultiplayerRoomSounds(),
|
new MultiplayerRoomSounds(),
|
||||||
@ -221,6 +225,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,224 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Threading;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.Multiplayer.Countdown;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||||
|
{
|
||||||
|
public class MatchStartControl : MultiplayerRoomComposite
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
|
private IDisposable clickOperation;
|
||||||
|
|
||||||
|
private Sample sampleReady;
|
||||||
|
private Sample sampleReadyAll;
|
||||||
|
private Sample sampleUnready;
|
||||||
|
|
||||||
|
private readonly BindableBool enabled = new BindableBool();
|
||||||
|
private readonly MultiplayerCountdownButton countdownButton;
|
||||||
|
private int countReady;
|
||||||
|
private ScheduledDelegate readySampleDelegate;
|
||||||
|
private IBindable<bool> operationInProgress;
|
||||||
|
|
||||||
|
public MatchStartControl()
|
||||||
|
{
|
||||||
|
InternalChild = new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
ColumnDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(),
|
||||||
|
new Dimension(GridSizeMode.AutoSize)
|
||||||
|
},
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
new MultiplayerReadyButton
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Size = Vector2.One,
|
||||||
|
Action = onReadyClick,
|
||||||
|
Enabled = { BindTarget = enabled },
|
||||||
|
},
|
||||||
|
countdownButton = new MultiplayerCountdownButton
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Size = new Vector2(40, 1),
|
||||||
|
Alpha = 0,
|
||||||
|
Action = startCountdown,
|
||||||
|
Enabled = { BindTarget = enabled }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(AudioManager audio)
|
||||||
|
{
|
||||||
|
operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy();
|
||||||
|
operationInProgress.BindValueChanged(_ => updateState());
|
||||||
|
|
||||||
|
sampleReady = audio.Samples.Get(@"Multiplayer/player-ready");
|
||||||
|
sampleReadyAll = audio.Samples.Get(@"Multiplayer/player-ready-all");
|
||||||
|
sampleUnready = audio.Samples.Get(@"Multiplayer/player-unready");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
CurrentPlaylistItem.BindValueChanged(_ => updateState());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnRoomUpdated()
|
||||||
|
{
|
||||||
|
base.OnRoomUpdated();
|
||||||
|
updateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnRoomLoadRequested()
|
||||||
|
{
|
||||||
|
base.OnRoomLoadRequested();
|
||||||
|
endOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onReadyClick()
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Debug.Assert(clickOperation == null);
|
||||||
|
clickOperation = ongoingOperationTracker.BeginOperation();
|
||||||
|
|
||||||
|
// Ensure the current user becomes ready before being able to do anything else (start match, stop countdown, unready).
|
||||||
|
if (!isReady() || !Client.IsHost)
|
||||||
|
{
|
||||||
|
toggleReady();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local user is the room host and is in a ready state.
|
||||||
|
// The only action they can take is to stop a countdown if one's currently running.
|
||||||
|
if (Room.Countdown != null)
|
||||||
|
{
|
||||||
|
stopCountdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// And if a countdown isn't running, start the match.
|
||||||
|
startMatch();
|
||||||
|
|
||||||
|
bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
|
||||||
|
|
||||||
|
void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation());
|
||||||
|
|
||||||
|
void stopCountdown() => Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation());
|
||||||
|
|
||||||
|
void startMatch() => Client.StartMatch().ContinueWith(t =>
|
||||||
|
{
|
||||||
|
// accessing Exception here silences any potential errors from the antecedent task
|
||||||
|
if (t.Exception != null)
|
||||||
|
{
|
||||||
|
// gameplay was not started due to an exception; unblock button.
|
||||||
|
endOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// gameplay is starting, the button will be unblocked on load requested.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startCountdown(TimeSpan duration)
|
||||||
|
{
|
||||||
|
Debug.Assert(clickOperation == null);
|
||||||
|
clickOperation = ongoingOperationTracker.BeginOperation();
|
||||||
|
|
||||||
|
Client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endOperation()
|
||||||
|
{
|
||||||
|
clickOperation?.Dispose();
|
||||||
|
clickOperation = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateState()
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
{
|
||||||
|
enabled.Value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var localUser = Client.LocalUser;
|
||||||
|
|
||||||
|
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
|
||||||
|
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
|
||||||
|
|
||||||
|
if (Room.Countdown != null)
|
||||||
|
countdownButton.Alpha = 0;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (localUser?.State)
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
countdownButton.Alpha = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MultiplayerUserState.Spectating:
|
||||||
|
case MultiplayerUserState.Ready:
|
||||||
|
countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled.Value =
|
||||||
|
Room.State == MultiplayerRoomState.Open
|
||||||
|
&& CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId
|
||||||
|
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
|
||||||
|
&& !operationInProgress.Value;
|
||||||
|
|
||||||
|
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready.
|
||||||
|
if (localUser?.State == MultiplayerUserState.Spectating)
|
||||||
|
enabled.Value &= Room.Host?.Equals(localUser) == true && newCountReady > 0;
|
||||||
|
|
||||||
|
if (newCountReady == countReady)
|
||||||
|
return;
|
||||||
|
|
||||||
|
readySampleDelegate?.Cancel();
|
||||||
|
readySampleDelegate = Schedule(() =>
|
||||||
|
{
|
||||||
|
if (newCountReady > countReady)
|
||||||
|
{
|
||||||
|
if (newCountReady == newCountTotal)
|
||||||
|
sampleReadyAll?.Play();
|
||||||
|
else
|
||||||
|
sampleReady?.Play();
|
||||||
|
}
|
||||||
|
else if (newCountReady < countReady)
|
||||||
|
{
|
||||||
|
sampleUnready?.Play();
|
||||||
|
}
|
||||||
|
|
||||||
|
countReady = newCountReady;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
// 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 Humanizer;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||||
|
{
|
||||||
|
public class MultiplayerCountdownButton : IconButton, IHasPopover
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan[] available_delays =
|
||||||
|
{
|
||||||
|
TimeSpan.FromSeconds(10),
|
||||||
|
TimeSpan.FromSeconds(30),
|
||||||
|
TimeSpan.FromMinutes(1),
|
||||||
|
TimeSpan.FromMinutes(2)
|
||||||
|
};
|
||||||
|
|
||||||
|
public new Action<TimeSpan> Action;
|
||||||
|
|
||||||
|
private readonly Drawable background;
|
||||||
|
|
||||||
|
public MultiplayerCountdownButton()
|
||||||
|
{
|
||||||
|
Icon = FontAwesome.Solid.CaretDown;
|
||||||
|
IconScale = new Vector2(0.6f);
|
||||||
|
|
||||||
|
Add(background = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Depth = float.MaxValue
|
||||||
|
});
|
||||||
|
|
||||||
|
base.Action = this.ShowPopover;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour colours)
|
||||||
|
{
|
||||||
|
background.Colour = colours.Green;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Popover GetPopover()
|
||||||
|
{
|
||||||
|
var flow = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Width = 200,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Spacing = new Vector2(2),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var duration in available_delays)
|
||||||
|
{
|
||||||
|
flow.Add(new OsuButton
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Text = $"Start match in {duration.Humanize()}",
|
||||||
|
BackgroundColour = background.Colour,
|
||||||
|
Action = () =>
|
||||||
|
{
|
||||||
|
Action(duration);
|
||||||
|
this.HidePopover();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OsuPopover { Child = flow };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -28,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
new MultiplayerReadyButton
|
new MatchStartControl
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
|
@ -2,210 +2,177 @@
|
|||||||
// 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;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Localisation;
|
||||||
using osu.Framework.Audio.Sample;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Backgrounds;
|
using osu.Game.Graphics.Backgrounds;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Screens.OnlinePlay.Components;
|
using osu.Game.Screens.OnlinePlay.Components;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||||
{
|
{
|
||||||
public class MultiplayerReadyButton : MultiplayerRoomComposite
|
public class MultiplayerReadyButton : ReadyButton
|
||||||
{
|
{
|
||||||
|
public new Triangles Triangles => base.Triangles;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private MultiplayerClient multiplayerClient { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuColour colours { get; set; }
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
|
||||||
|
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
private IDisposable clickOperation;
|
private MultiplayerRoom room => multiplayerClient.Room;
|
||||||
|
|
||||||
private Sample sampleReady;
|
|
||||||
private Sample sampleReadyAll;
|
|
||||||
private Sample sampleUnready;
|
|
||||||
|
|
||||||
private readonly ButtonWithTrianglesExposed button;
|
|
||||||
private int countReady;
|
|
||||||
private ScheduledDelegate readySampleDelegate;
|
|
||||||
private IBindable<bool> operationInProgress;
|
|
||||||
|
|
||||||
public MultiplayerReadyButton()
|
|
||||||
{
|
|
||||||
InternalChild = button = new ButtonWithTrianglesExposed
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Size = Vector2.One,
|
|
||||||
Action = onReadyClick,
|
|
||||||
Enabled = { Value = true },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(AudioManager audio)
|
|
||||||
{
|
|
||||||
operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy();
|
|
||||||
operationInProgress.BindValueChanged(_ => updateState());
|
|
||||||
|
|
||||||
sampleReady = audio.Samples.Get(@"Multiplayer/player-ready");
|
|
||||||
sampleReadyAll = audio.Samples.Get(@"Multiplayer/player-ready-all");
|
|
||||||
sampleUnready = audio.Samples.Get(@"Multiplayer/player-unready");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
CurrentPlaylistItem.BindValueChanged(_ => updateState());
|
multiplayerClient.RoomUpdated += onRoomUpdated;
|
||||||
|
onRoomUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnRoomUpdated()
|
private MultiplayerCountdown countdown;
|
||||||
|
private DateTimeOffset countdownReceivedTime;
|
||||||
|
private ScheduledDelegate countdownUpdateDelegate;
|
||||||
|
|
||||||
|
private void onRoomUpdated() => Scheduler.AddOnce(() =>
|
||||||
{
|
{
|
||||||
base.OnRoomUpdated();
|
if (countdown == null && room?.Countdown != null)
|
||||||
updateState();
|
countdownReceivedTime = DateTimeOffset.Now;
|
||||||
|
|
||||||
|
countdown = room?.Countdown;
|
||||||
|
|
||||||
|
if (room?.Countdown != null)
|
||||||
|
countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 1000, true);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
countdownUpdateDelegate?.Cancel();
|
||||||
|
countdownUpdateDelegate = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnRoomLoadRequested()
|
updateButtonText();
|
||||||
{
|
updateButtonColour();
|
||||||
base.OnRoomLoadRequested();
|
|
||||||
endOperation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onReadyClick()
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Debug.Assert(clickOperation == null);
|
|
||||||
clickOperation = ongoingOperationTracker.BeginOperation();
|
|
||||||
|
|
||||||
// Ensure the current user becomes ready before being able to do anything else (start match, stop countdown, unready).
|
|
||||||
if (!isReady() || !Client.IsHost)
|
|
||||||
{
|
|
||||||
toggleReady();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// And if a countdown isn't running, start the match.
|
|
||||||
startMatch();
|
|
||||||
|
|
||||||
bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
|
|
||||||
|
|
||||||
void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation());
|
|
||||||
|
|
||||||
void startMatch() => Client.StartMatch().ContinueWith(t =>
|
|
||||||
{
|
|
||||||
// accessing Exception here silences any potential errors from the antecedent task
|
|
||||||
if (t.Exception != null)
|
|
||||||
{
|
|
||||||
// gameplay was not started due to an exception; unblock button.
|
|
||||||
endOperation();
|
|
||||||
}
|
|
||||||
|
|
||||||
// gameplay is starting, the button will be unblocked on load requested.
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private void updateButtonText()
|
||||||
|
{
|
||||||
|
if (room == null)
|
||||||
|
{
|
||||||
|
Text = "Ready";
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void endOperation()
|
var localUser = multiplayerClient.LocalUser;
|
||||||
{
|
|
||||||
clickOperation?.Dispose();
|
|
||||||
clickOperation = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateState()
|
int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready);
|
||||||
{
|
int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
|
||||||
var localUser = Client.LocalUser;
|
string countText = $"({countReady} / {countTotal} ready)";
|
||||||
|
|
||||||
int newCountReady = Room?.Users.Count(u => u.State == MultiplayerUserState.Ready) ?? 0;
|
if (countdown != null)
|
||||||
int newCountTotal = Room?.Users.Count(u => u.State != MultiplayerUserState.Spectating) ?? 0;
|
{
|
||||||
|
TimeSpan timeElapsed = DateTimeOffset.Now - countdownReceivedTime;
|
||||||
|
TimeSpan countdownRemaining;
|
||||||
|
|
||||||
|
if (timeElapsed > countdown.TimeRemaining)
|
||||||
|
countdownRemaining = TimeSpan.Zero;
|
||||||
|
else
|
||||||
|
countdownRemaining = countdown.TimeRemaining - timeElapsed;
|
||||||
|
|
||||||
|
string countdownText = $"Starting in {countdownRemaining:mm\\:ss}";
|
||||||
|
|
||||||
switch (localUser?.State)
|
switch (localUser?.State)
|
||||||
{
|
{
|
||||||
default:
|
default:
|
||||||
button.Text = "Ready";
|
Text = $"Ready ({countdownText.ToLowerInvariant()})";
|
||||||
updateButtonColour(true);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MultiplayerUserState.Spectating:
|
case MultiplayerUserState.Spectating:
|
||||||
case MultiplayerUserState.Ready:
|
case MultiplayerUserState.Ready:
|
||||||
string countText = $"({newCountReady} / {newCountTotal} ready)";
|
Text = $"{countdownText} {countText}";
|
||||||
|
break;
|
||||||
if (Room?.Host?.Equals(localUser) == true)
|
}
|
||||||
{
|
|
||||||
button.Text = $"Start match {countText}";
|
|
||||||
updateButtonColour(true);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
button.Text = $"Waiting for host... {countText}";
|
switch (localUser?.State)
|
||||||
updateButtonColour(false);
|
{
|
||||||
|
default:
|
||||||
|
Text = "Ready";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MultiplayerUserState.Spectating:
|
||||||
|
case MultiplayerUserState.Ready:
|
||||||
|
Text = room.Host?.Equals(localUser) == true
|
||||||
|
? $"Start match {countText}"
|
||||||
|
: $"Waiting for host... {countText}";
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateButtonColour()
|
||||||
|
{
|
||||||
|
if (room == null)
|
||||||
|
{
|
||||||
|
setGreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var localUser = multiplayerClient.LocalUser;
|
||||||
|
|
||||||
|
switch (localUser?.State)
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
setGreen();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MultiplayerUserState.Spectating:
|
||||||
|
case MultiplayerUserState.Ready:
|
||||||
|
if (room?.Host?.Equals(localUser) == true && room.Countdown == null)
|
||||||
|
setGreen();
|
||||||
|
else
|
||||||
|
setYellow();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool enableButton =
|
void setYellow()
|
||||||
Room?.State == MultiplayerRoomState.Open
|
|
||||||
&& CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId
|
|
||||||
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
|
|
||||||
&& !operationInProgress.Value;
|
|
||||||
|
|
||||||
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready.
|
|
||||||
if (localUser?.State == MultiplayerUserState.Spectating)
|
|
||||||
enableButton &= Room?.Host?.Equals(localUser) == true && newCountReady > 0;
|
|
||||||
|
|
||||||
button.Enabled.Value = enableButton;
|
|
||||||
|
|
||||||
if (newCountReady == countReady)
|
|
||||||
return;
|
|
||||||
|
|
||||||
readySampleDelegate?.Cancel();
|
|
||||||
readySampleDelegate = Schedule(() =>
|
|
||||||
{
|
{
|
||||||
if (newCountReady > countReady)
|
BackgroundColour = colours.YellowDark;
|
||||||
{
|
Triangles.ColourDark = colours.YellowDark;
|
||||||
if (newCountReady == newCountTotal)
|
Triangles.ColourLight = colours.Yellow;
|
||||||
sampleReadyAll?.Play();
|
|
||||||
else
|
|
||||||
sampleReady?.Play();
|
|
||||||
}
|
|
||||||
else if (newCountReady < countReady)
|
|
||||||
{
|
|
||||||
sampleUnready?.Play();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
countReady = newCountReady;
|
void setGreen()
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateButtonColour(bool green)
|
|
||||||
{
|
{
|
||||||
if (green)
|
BackgroundColour = colours.Green;
|
||||||
{
|
Triangles.ColourDark = colours.Green;
|
||||||
button.BackgroundColour = colours.Green;
|
Triangles.ColourLight = colours.GreenLight;
|
||||||
button.Triangles.ColourDark = colours.Green;
|
|
||||||
button.Triangles.ColourLight = colours.GreenLight;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
button.BackgroundColour = colours.YellowDark;
|
|
||||||
button.Triangles.ColourDark = colours.YellowDark;
|
|
||||||
button.Triangles.ColourLight = colours.Yellow;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ButtonWithTrianglesExposed : ReadyButton
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
public new Triangles Triangles => base.Triangles;
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (multiplayerClient != null)
|
||||||
|
multiplayerClient.RoomUpdated -= onRoomUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override LocalisableString TooltipText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready)
|
||||||
|
return "Cancel countdown";
|
||||||
|
|
||||||
|
return base.TooltipText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,15 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Development;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
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.Countdown;
|
||||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
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;
|
||||||
@ -114,12 +117,24 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
public void ChangeUserState(int userId, MultiplayerUserState newState)
|
public void ChangeUserState(int userId, MultiplayerUserState newState)
|
||||||
{
|
{
|
||||||
Debug.Assert(Room != null);
|
Debug.Assert(Room != null);
|
||||||
|
|
||||||
((IMultiplayerClient)this).UserStateChanged(userId, newState);
|
((IMultiplayerClient)this).UserStateChanged(userId, newState);
|
||||||
|
|
||||||
Schedule(() =>
|
Schedule(() =>
|
||||||
{
|
{
|
||||||
switch (Room.State)
|
switch (Room.State)
|
||||||
{
|
{
|
||||||
|
case MultiplayerRoomState.Open:
|
||||||
|
// If there are no remaining ready users or the host is not ready, stop any existing countdown.
|
||||||
|
// Todo: When we have an "automatic start" mode, this should also start a new countdown if any users _are_ ready.
|
||||||
|
// Todo: This doesn't yet support non-match-start countdowns.
|
||||||
|
bool shouldStopCountdown = Room.Users.All(u => u.State != MultiplayerUserState.Ready);
|
||||||
|
shouldStopCountdown |= Room.Host?.State != MultiplayerUserState.Ready && Room.Host?.State != MultiplayerUserState.Spectating;
|
||||||
|
|
||||||
|
if (shouldStopCountdown)
|
||||||
|
countdownStopSource?.Cancel();
|
||||||
|
break;
|
||||||
|
|
||||||
case MultiplayerRoomState.WaitingForLoad:
|
case MultiplayerRoomState.WaitingForLoad:
|
||||||
if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
|
if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
|
||||||
{
|
{
|
||||||
@ -282,6 +297,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CancellationTokenSource? countdownSkipSource;
|
||||||
|
private CancellationTokenSource? countdownStopSource;
|
||||||
|
private Task countdownTask = Task.CompletedTask;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Skips to the end of the currently-running countdown, if one is running,
|
||||||
|
/// and runs the callback (e.g. to start the match) as soon as possible unless the countdown has been cancelled.
|
||||||
|
/// </summary>
|
||||||
|
public void SkipToEndOfCountdown() => countdownSkipSource?.Cancel();
|
||||||
|
|
||||||
public override async Task SendMatchRequest(MatchUserRequest request)
|
public override async Task SendMatchRequest(MatchUserRequest request)
|
||||||
{
|
{
|
||||||
Debug.Assert(Room != null);
|
Debug.Assert(Room != null);
|
||||||
@ -289,6 +314,67 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
switch (request)
|
switch (request)
|
||||||
{
|
{
|
||||||
|
case StartMatchCountdownRequest matchCountdownRequest:
|
||||||
|
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||||
|
|
||||||
|
countdownStopSource?.Cancel();
|
||||||
|
|
||||||
|
// Note that this will leak CTSs, however this is a test method and we haven't noticed foregoing disposal of non-linked CTSs to be detrimental.
|
||||||
|
// If necessary, this can be moved into the final schedule below, and the class-level fields be nulled out accordingly.
|
||||||
|
var stopSource = countdownStopSource = new CancellationTokenSource();
|
||||||
|
var skipSource = countdownSkipSource = new CancellationTokenSource();
|
||||||
|
var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration };
|
||||||
|
|
||||||
|
Task lastCountdownTask = countdownTask;
|
||||||
|
countdownTask = start();
|
||||||
|
|
||||||
|
async Task start()
|
||||||
|
{
|
||||||
|
await lastCountdownTask;
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (stopSource.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Room.Countdown = countdown;
|
||||||
|
MatchEvent(new CountdownChangedEvent { Countdown = countdown });
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token))
|
||||||
|
await Task.Delay(matchCountdownRequest.Duration, cancellationSource.Token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Clients need to be notified of cancellations in the following code.
|
||||||
|
}
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room.Countdown != countdown)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Room.Countdown = null;
|
||||||
|
MatchEvent(new CountdownChangedEvent { Countdown = null });
|
||||||
|
|
||||||
|
if (stopSource.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
StartMatch().WaitSafely();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case StopCountdownRequest _:
|
||||||
|
countdownStopSource?.Cancel();
|
||||||
|
|
||||||
|
Room.Countdown = null;
|
||||||
|
await MatchEvent(new CountdownChangedEvent { Countdown = Room.Countdown });
|
||||||
|
break;
|
||||||
|
|
||||||
case ChangeTeamRequest changeTeam:
|
case ChangeTeamRequest changeTeam:
|
||||||
|
|
||||||
TeamVersusRoomState roomState = (TeamVersusRoomState)Room.MatchState!;
|
TeamVersusRoomState roomState = (TeamVersusRoomState)Room.MatchState!;
|
||||||
|
Loading…
Reference in New Issue
Block a user