Merge branch 'master' into fix-popup-dialog-handling-exit-sequence

This commit is contained in:
Bartłomiej Dach 2022-04-16 19:01:18 +02:00 committed by GitHub
commit 15c54b38c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 604 additions and 346 deletions

View File

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="SwUserDefinedSpecifications">
<option name="specTypeByUrl">
<map />
</option>
</component>
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent"> <component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
<option name="ENSURE_MISC_FILE_EXISTS" value="true" /> <option name="ENSURE_MISC_FILE_EXISTS" value="true" />
</component> </component>

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.407.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.415.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.408.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.415.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

@ -26,6 +26,12 @@ namespace osu.Game.Tests.Gameplay
Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage)); Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
} }
[SetUpSteps]
public void SetUpSteps()
{
AddStep("reset audio offset", () => localConfig.SetValue(OsuSetting.AudioOffset, 0.0));
}
[Test] [Test]
public void TestStartThenElapsedTime() public void TestStartThenElapsedTime()
{ {
@ -36,7 +42,7 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack(); working.LoadTrack();
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0)); Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
}); });
AddStep("start clock", () => gameplayClockContainer.Start()); AddStep("start clock", () => gameplayClockContainer.Start());
@ -53,7 +59,7 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack(); working.LoadTrack();
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0)); Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
}); });
AddStep("start clock", () => gameplayClockContainer.Start()); AddStep("start clock", () => gameplayClockContainer.Start());
@ -73,26 +79,29 @@ namespace osu.Game.Tests.Gameplay
public void TestSeekPerformsInGameplayTime( public void TestSeekPerformsInGameplayTime(
[Values(1.0, 0.5, 2.0)] double clockRate, [Values(1.0, 0.5, 2.0)] double clockRate,
[Values(0.0, 200.0, -200.0)] double userOffset, [Values(0.0, 200.0, -200.0)] double userOffset,
[Values(false, true)] bool whileStopped) [Values(false, true)] bool whileStopped,
[Values(false, true)] bool setAudioOffsetBeforeConstruction)
{ {
ClockBackedTestWorkingBeatmap working = null; ClockBackedTestWorkingBeatmap working = null;
GameplayClockContainer gameplayClockContainer = null; GameplayClockContainer gameplayClockContainer = null;
if (setAudioOffsetBeforeConstruction)
AddStep($"preset audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
AddStep("create container", () => AddStep("create container", () =>
{ {
working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio); working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio);
working.LoadTrack(); working.LoadTrack();
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0)); Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
if (whileStopped) gameplayClockContainer.Reset(startClock: !whileStopped);
gameplayClockContainer.Stop();
gameplayClockContainer.Reset();
}); });
AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(clockRate))); AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(clockRate)));
AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
if (!setAudioOffsetBeforeConstruction)
AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500)); AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500));
AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f)); AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f));

View File

@ -88,7 +88,7 @@ namespace osu.Game.Tests.Gameplay
[Test] [Test]
public void TestSampleHasLifetimeEndWithInitialClockTime() public void TestSampleHasLifetimeEndWithInitialClockTime()
{ {
GameplayClockContainer gameplayContainer = null; MasterGameplayClockContainer gameplayContainer = null;
DrawableStoryboardSample sample = null; DrawableStoryboardSample sample = null;
AddStep("create container", () => AddStep("create container", () =>
@ -96,8 +96,11 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack(); working.LoadTrack();
Add(gameplayContainer = new MasterGameplayClockContainer(working, 1000, true) const double start_time = 1000;
Add(gameplayContainer = new MasterGameplayClockContainer(working, start_time)
{ {
StartTime = start_time,
IsPaused = { Value = true }, IsPaused = { Value = true },
Child = new FrameStabilityContainer Child = new FrameStabilityContainer
{ {

View File

@ -56,10 +56,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private double lastFrequency = double.MaxValue; private double lastFrequency = double.MaxValue;
protected override void Update() protected override void UpdateAfterChildren()
{ {
base.Update(); base.UpdateAfterChildren();
// This must be done in UpdateAfterChildren to allow the gameplay clock to have updated before checking values.
double freq = Beatmap.Value.Track.AggregateFrequency.Value; double freq = Beatmap.Value.Track.AggregateFrequency.Value;
FrequencyIncreased |= freq > lastFrequency; FrequencyIncreased |= freq > lastFrequency;

View File

@ -1,12 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
@ -36,10 +34,10 @@ namespace osu.Game.Tests.Visual.Gameplay
BeatmapInfo = { AudioLeadIn = leadIn } BeatmapInfo = { AudioLeadIn = leadIn }
}); });
AddAssert($"first frame is {expectedStartTime}", () => AddStep("check first frame time", () =>
{ {
Debug.Assert(player.FirstFrameClockTime != null); Assert.That(player.FirstFrameClockTime, Is.Not.Null);
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms); Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
}); });
} }
@ -59,10 +57,10 @@ namespace osu.Game.Tests.Visual.Gameplay
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard); loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
AddAssert($"first frame is {expectedStartTime}", () => AddStep("check first frame time", () =>
{ {
Debug.Assert(player.FirstFrameClockTime != null); Assert.That(player.FirstFrameClockTime, Is.Not.Null);
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms); Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
}); });
} }
@ -97,10 +95,10 @@ namespace osu.Game.Tests.Visual.Gameplay
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard); loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
AddAssert($"first frame is {expectedStartTime}", () => AddStep("check first frame time", () =>
{ {
Debug.Assert(player.FirstFrameClockTime != null); Assert.That(player.FirstFrameClockTime, Is.Not.Null);
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms); Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
}); });
} }

View File

@ -3,80 +3,155 @@
using System; using System;
using System.Linq; using System.Linq;
using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform; using osu.Framework.Logging;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneMatchStartControl : MultiplayerTestScene public class TestSceneMatchStartControl : OsuManualInputManagerTestScene
{ {
private readonly Mock<MultiplayerClient> multiplayerClient = new Mock<MultiplayerClient>();
private readonly Mock<OnlinePlayBeatmapAvailabilityTracker> availabilityTracker = new Mock<OnlinePlayBeatmapAvailabilityTracker>();
private readonly Bindable<BeatmapAvailability> beatmapAvailability = new Bindable<BeatmapAvailability>();
private readonly Bindable<Room> room = new Bindable<Room>();
private MultiplayerRoom multiplayerRoom;
private MultiplayerRoomUser localUser;
private OngoingOperationTracker ongoingOperationTracker;
private PopoverContainer content;
private MatchStartControl control; private MatchStartControl control;
private BeatmapSetInfo importedSet;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>(); private OsuButton readyButton => control.ChildrenOfType<OsuButton>().Single();
private BeatmapManager beatmaps; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
private RulesetStore rulesets; new CachedModelDependencyContainer<Room>(base.CreateChildDependencies(parent)) { Model = { BindTarget = room } };
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load()
{ {
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.CacheAs(multiplayerClient.Object);
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.CacheAs(ongoingOperationTracker = new OngoingOperationTracker());
Dependencies.Cache(Realm); Dependencies.CacheAs(availabilityTracker.Object);
availabilityTracker.SetupGet(a => a.Availability).Returns(beatmapAvailability);
multiplayerClient.SetupGet(m => m.LocalUser).Returns(() => localUser);
multiplayerClient.SetupGet(m => m.Room).Returns(() => multiplayerRoom);
// By default, the local user is to be the host.
multiplayerClient.SetupGet(m => m.IsHost).Returns(() => ReferenceEquals(multiplayerRoom.Host, localUser));
// Assume all state changes are accepted by the server.
multiplayerClient.Setup(m => m.ChangeState(It.IsAny<MultiplayerUserState>()))
.Callback((MultiplayerUserState r) =>
{
Logger.Log($"Changing local user state from {localUser.State} to {r}");
localUser.State = r;
raiseRoomUpdated();
});
multiplayerClient.Setup(m => m.StartMatch())
.Callback(() =>
{
multiplayerClient.Raise(m => m.LoadRequested -= null);
// immediately "end" gameplay, as we don't care about that part of the process.
changeUserState(localUser.UserID, MultiplayerUserState.Idle);
});
multiplayerClient.Setup(m => m.SendMatchRequest(It.IsAny<MatchUserRequest>()))
.Callback((MatchUserRequest request) =>
{
switch (request)
{
case StartMatchCountdownRequest countdownStart:
setRoomCountdown(countdownStart.Duration);
break;
case StopCountdownRequest _:
multiplayerRoom.Countdown = null;
raiseRoomUpdated();
break;
}
});
Children = new Drawable[]
{
ongoingOperationTracker,
content = new PopoverContainer { RelativeSizeAxes = Axes.Both }
};
} }
[SetUp] [SetUpSteps]
public new void Setup() => Schedule(() => public void SetUpSteps()
{ {
AvailabilityTracker.SelectedItem.BindTo(selectedItem); AddStep("reset state", () =>
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 multiplayerClient.Invocations.Clear();
};
Child = new PopoverContainer beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable();
var playlistItem = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
};
room.Value = new Room
{
Playlist = { playlistItem },
CurrentPlaylistItem = { Value = playlistItem }
};
localUser = new MultiplayerRoomUser(API.LocalUser.Value.Id) { User = API.LocalUser.Value };
multiplayerRoom = new MultiplayerRoom(0)
{
Playlist =
{
new MultiplayerPlaylistItem(playlistItem),
},
Users = { localUser },
Host = localUser,
};
});
AddStep("create control", () =>
{ {
RelativeSizeAxes = Axes.Both, content.Child = control = new MatchStartControl
Child = control = new MatchStartControl
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(250, 50), Size = new Vector2(250, 50),
} };
}; });
}); }
[Test] [Test]
public void TestStartWithCountdown() public void TestStartWithCountdown()
{ {
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true); AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>(); ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () => AddStep("click the first countdown button", () =>
{ {
@ -85,8 +160,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddStep("check request received", () =>
AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); {
multiplayerClient.Verify(m => m.SendMatchRequest(It.Is<StartMatchCountdownRequest>(req =>
req.Duration == TimeSpan.FromSeconds(10)
)), Times.Once);
});
} }
[Test] [Test]
@ -94,6 +173,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true); AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>(); ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () => AddStep("click the first countdown button", () =>
{ {
@ -102,6 +182,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddStep("check request received", () =>
{
multiplayerClient.Verify(m => m.SendMatchRequest(It.Is<StartMatchCountdownRequest>(req =>
req.Duration == TimeSpan.FromSeconds(10)
)), Times.Once);
});
ClickButtonWhenEnabled<MultiplayerCountdownButton>(); ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the cancel button", () => AddStep("click the cancel button", () =>
{ {
@ -110,41 +197,39 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddStep("check request received", () =>
AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); {
multiplayerClient.Verify(m => m.SendMatchRequest(It.IsAny<StopCountdownRequest>()), Times.Once);
});
} }
[Test] [Test]
public void TestReadyAndUnReadyDuringCountdown() public void TestReadyAndUnReadyDuringCountdown()
{ {
AddStep("add second user as host", () => AddStep("add second user as host", () => addUser(new APIUser { Id = 2, Username = "Another user" }, true));
{
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()); AddStep("start countdown", () => setRoomCountdown(TimeSpan.FromMinutes(1)));
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); checkLocalUserState(MultiplayerUserState.Idle);
} }
[Test] [Test]
public void TestCountdownWhileSpectating() public void TestCountdownWhileSpectating()
{ {
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); AddStep("set spectating", () => changeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); checkLocalUserState(MultiplayerUserState.Spectating);
AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent); AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value); AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); AddStep("add second user", () => addUser(new APIUser { Id = 2, Username = "Another user" }));
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value); AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); AddStep("set second user ready", () => changeUserState(2, MultiplayerUserState.Ready));
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value); AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
} }
@ -153,60 +238,54 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("add second user as host", () => AddStep("add second user as host", () =>
{ {
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); addUser(new APIUser { Id = 2, Username = "Another user" }, true);
MultiplayerClient.TransferHost(2);
}); });
AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely()); AddStep("start countdown", () => multiplayerClient.Object.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely());
AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null); AddUntilStep("countdown started", () => multiplayerRoom.Countdown != null);
AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID)); AddStep("transfer host to local user", () => transferHost(localUser));
AddUntilStep("local user is host", () => MultiplayerClient.Room?.Host?.Equals(MultiplayerClient.LocalUser) == true); AddUntilStep("local user is host", () => multiplayerRoom.Host?.Equals(multiplayerClient.Object.LocalUser) == true);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null); AddAssert("countdown still active", () => multiplayerRoom.Countdown != null);
} }
[Test] [Test]
public void TestCountdownButtonVisibilityWithAutoStartEnablement() public void TestCountdownButtonVisibilityWithAutoStart()
{ {
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
AddUntilStep("countdown button visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent); AddUntilStep("countdown button visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely()); AddStep("enable auto start", () => changeRoomSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }));
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
AddUntilStep("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent); AddUntilStep("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
} }
[Test] [Test]
public void TestClickingReadyButtonUnReadiesDuringAutoStart() public void TestClickingReadyButtonUnReadiesDuringAutoStart()
{ {
AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely()); AddStep("enable auto start", () => changeRoomSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }));
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); checkLocalUserState(MultiplayerUserState.Idle);
} }
[Test] [Test]
public void TestDeletedBeatmapDisableReady() public void TestDeletedBeatmapDisableReady()
{ {
OsuButton readyButton = null; AddUntilStep("ready button enabled", () => readyButton.Enabled.Value);
AddUntilStep("ensure ready button enabled", () => AddStep("mark beatmap not available", () => beatmapAvailability.Value = BeatmapAvailability.NotDownloaded());
{
readyButton = control.ChildrenOfType<OsuButton>().Single();
return readyButton.Enabled.Value;
});
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value); AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
AddStep("mark beatmap available", () => beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable());
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value); AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
} }
@ -215,31 +294,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("add second user as host", () => AddStep("add second user as host", () =>
{ {
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); addUser(new APIUser { Id = 2, Username = "Another user" }, true);
MultiplayerClient.TransferHost(2);
}); });
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); checkLocalUserState(MultiplayerUserState.Idle);
} }
[TestCase(true)] [TestCase(true)]
[TestCase(false)] [TestCase(false)]
public void TestToggleStateWhenHost(bool allReady) public void TestToggleStateWhenHost(bool allReady)
{ {
AddStep("setup", () => if (!allReady)
{ AddStep("add other user", () => addUser(new APIUser { Id = 2, Username = "Another user" }));
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
if (!allReady)
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
});
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
verifyGameplayStartFlow(); verifyGameplayStartFlow();
} }
@ -249,12 +322,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("add host", () => AddStep("add host", () =>
{ {
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); addUser(new APIUser { Id = 2, Username = "Another user" }, true);
MultiplayerClient.TransferHost(2);
}); });
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
AddStep("make local user host", () => transferHost(localUser));
verifyGameplayStartFlow(); verifyGameplayStartFlow();
} }
@ -264,18 +337,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("setup", () => AddStep("setup", () =>
{ {
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0); addUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
}); });
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0)); AddStep("transfer host", () => transferHost(multiplayerRoom.Users[1]));
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); checkLocalUserState(MultiplayerUserState.Idle);
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value); AddUntilStep("ready button enabled", () => readyButton.Enabled.Value);
} }
[TestCase(true)] [TestCase(true)]
@ -283,44 +355,83 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestManyUsersChangingState(bool isHost) public void TestManyUsersChangingState(bool isHost)
{ {
const int users = 10; 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("add many users", () =>
AddStep("transfer host", () => MultiplayerClient.TransferHost(2)); {
for (int i = 0; i < users; i++)
addUser(new APIUser { Id = i, Username = "Another user" }, !isHost && i == 2);
});
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddRepeatStep("change user ready state", () => AddRepeatStep("change user ready state", () =>
{ {
MultiplayerClient.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle); changeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle);
}, 20); }, 20);
AddRepeatStep("ready all users", () => AddRepeatStep("ready all users", () =>
{ {
var nextUnready = MultiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); var nextUnready = multiplayerRoom.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
if (nextUnready != null) if (nextUnready != null)
MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); changeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
}, users); }, users);
} }
private void verifyGameplayStartFlow() private void verifyGameplayStartFlow()
{ {
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
AddStep("finish gameplay", () => AddStep("check start request received", () => multiplayerClient.Verify(m => m.StartMatch(), Times.Once));
{
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);
} }
private void checkLocalUserState(MultiplayerUserState state) =>
AddUntilStep($"local user is {state}", () => localUser.State == state);
private void setRoomCountdown(TimeSpan duration)
{
multiplayerRoom.Countdown = new MatchStartCountdown { TimeRemaining = duration };
raiseRoomUpdated();
}
private void changeUserState(int userId, MultiplayerUserState newState)
{
multiplayerRoom.Users.Single(u => u.UserID == userId).State = newState;
raiseRoomUpdated();
}
private void addUser(APIUser user, bool asHost = false)
{
var multiplayerRoomUser = new MultiplayerRoomUser(user.Id) { User = user };
multiplayerRoom.Users.Add(multiplayerRoomUser);
if (asHost)
transferHost(multiplayerRoomUser);
raiseRoomUpdated();
}
private void transferHost(MultiplayerRoomUser user)
{
multiplayerRoom.Host = user;
raiseRoomUpdated();
}
private void changeRoomSettings(MultiplayerRoomSettings settings)
{
multiplayerRoom.Settings = settings;
// Changing settings should reset all user ready statuses.
foreach (var user in multiplayerRoom.Users)
{
if (user.State == MultiplayerUserState.Ready)
user.State = MultiplayerUserState.Idle;
}
raiseRoomUpdated();
}
private void raiseRoomUpdated() => multiplayerClient.Raise(m => m.RoomUpdated -= null);
} }
} }

View File

@ -464,16 +464,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
private class TestMultiSpectatorScreen : MultiSpectatorScreen private class TestMultiSpectatorScreen : MultiSpectatorScreen
{ {
private readonly double? gameplayStartTime; private readonly double? startTime;
public TestMultiSpectatorScreen(Room room, MultiplayerRoomUser[] users, double? gameplayStartTime = null) public TestMultiSpectatorScreen(Room room, MultiplayerRoomUser[] users, double? startTime = null)
: base(room, users) : base(room, users)
{ {
this.gameplayStartTime = gameplayStartTime; this.startTime = startTime;
} }
protected override MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap) protected override MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap)
=> new MasterGameplayClockContainer(beatmap, gameplayStartTime ?? 0, gameplayStartTime.HasValue); => new MasterGameplayClockContainer(beatmap, 0) { StartTime = startTime ?? 0 };
} }
} }
} }

View File

@ -495,17 +495,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true); AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); AddAssert("Mods match current item",
() => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
AddStep("Switch required mods", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Mods.Value = new Mod[] { new OsuModDoubleTime() }); AddStep("Switch required mods", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Mods.Value = new Mod[] { new OsuModDoubleTime() });
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().WaitSafely()); AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); AddAssert("Mods match current item",
() => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
} }
[Test] [Test]
@ -665,6 +668,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen); AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen);
} }
[Test]
public void TestGameplayDoesntStartWithNonLoadedUser()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
}
}
});
pressReadyButton();
AddStep("join other user and ready", () =>
{
multiplayerClient.AddUser(new APIUser { Id = 1234 });
multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready);
});
AddStep("start match", () =>
{
multiplayerClient.StartMatch();
});
AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player);
AddWaitStep("wait some", 20);
AddAssert("ensure gameplay hasn't started", () => this.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.IsRunning == false);
}
[Test] [Test]
public void TestRoomSettingsReQueriedWhenJoiningRoom() public void TestRoomSettingsReQueriedWhenJoiningRoom()
{ {

View File

@ -0,0 +1,114 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public class TestSceneScalingContainer : OsuTestScene
{
private OsuConfigManager osuConfigManager { get; set; }
private ScalingContainer scaling1;
private ScalingContainer scaling2;
private Box scaleTarget;
[BackgroundDependencyLoader]
private void load()
{
osuConfigManager = new OsuConfigManager(LocalStorage);
Dependencies.CacheAs(osuConfigManager);
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
scaling1 = new ScalingContainer(ScalingMode.Everything)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(0.8f),
Children = new Drawable[]
{
scaling2 = new ScalingContainer(ScalingMode.Everything)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(0.8f),
Children = new Drawable[]
{
new Box
{
Colour = Color4.Purple,
RelativeSizeAxes = Axes.Both,
},
scaleTarget = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.White,
Size = new Vector2(100),
},
}
}
}
}
}
},
};
}
[Test]
public void TestScaling()
{
AddStep("adjust scale", () => osuConfigManager.SetValue(OsuSetting.UIScale, 2f));
checkForCorrectness();
AddStep("adjust scale", () => osuConfigManager.SetValue(OsuSetting.UIScale, 0.5f));
checkForCorrectness();
}
private void checkForCorrectness()
{
Quad? scaling1LastQuad = null;
Quad? scaling2LastQuad = null;
Quad? scalingTargetLastQuad = null;
AddUntilStep("ensure dimensions don't change", () =>
{
if (scaling1LastQuad.HasValue && scaling2LastQuad.HasValue)
{
// check inter-frame changes to make sure they match expectations.
Assert.That(scaling1.ScreenSpaceDrawQuad.AlmostEquals(scaling1LastQuad.Value), Is.True);
Assert.That(scaling2.ScreenSpaceDrawQuad.AlmostEquals(scaling2LastQuad.Value), Is.True);
}
scaling1LastQuad = scaling1.ScreenSpaceDrawQuad;
scaling2LastQuad = scaling2.ScreenSpaceDrawQuad;
// wait for scaling to stop.
bool scalingFinished = scalingTargetLastQuad.HasValue && scaleTarget.ScreenSpaceDrawQuad.AlmostEquals(scalingTargetLastQuad.Value);
scalingTargetLastQuad = scaleTarget.ScreenSpaceDrawQuad;
return scalingFinished;
});
}
}
}

View File

@ -26,6 +26,11 @@ namespace osu.Game.Beatmaps
{ {
private readonly WeakList<BeatmapManagerWorkingBeatmap> workingCache = new WeakList<BeatmapManagerWorkingBeatmap>(); private readonly WeakList<BeatmapManagerWorkingBeatmap> workingCache = new WeakList<BeatmapManagerWorkingBeatmap>();
/// <summary>
/// Beatmap files may specify this filename to denote that they don't have an audio track.
/// </summary>
private const string virtual_track_filename = @"virtual";
/// <summary> /// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available. /// A default representation of a WorkingBeatmap to use when no beatmap is available.
/// </summary> /// </summary>
@ -40,7 +45,8 @@ namespace osu.Game.Beatmaps
[CanBeNull] [CanBeNull]
private readonly GameHost host; private readonly GameHost host;
public WorkingBeatmapCache(ITrackStore trackStore, AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> files, WorkingBeatmap defaultBeatmap = null, GameHost host = null) public WorkingBeatmapCache(ITrackStore trackStore, AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> files, WorkingBeatmap defaultBeatmap = null,
GameHost host = null)
{ {
DefaultBeatmap = defaultBeatmap; DefaultBeatmap = defaultBeatmap;
@ -157,6 +163,9 @@ namespace osu.Game.Beatmaps
if (string.IsNullOrEmpty(Metadata?.AudioFile)) if (string.IsNullOrEmpty(Metadata?.AudioFile))
return null; return null;
if (Metadata.AudioFile == virtual_track_filename)
return null;
try try
{ {
return resources.Tracks.Get(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile)); return resources.Tracks.Get(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
@ -173,6 +182,9 @@ namespace osu.Game.Beatmaps
if (string.IsNullOrEmpty(Metadata?.AudioFile)) if (string.IsNullOrEmpty(Metadata?.AudioFile))
return null; return null;
if (Metadata.AudioFile == virtual_track_filename)
return null;
try try
{ {
var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile)); var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));

View File

@ -21,6 +21,8 @@ namespace osu.Game.Graphics.Containers
/// </summary> /// </summary>
public class ScalingContainer : Container public class ScalingContainer : Container
{ {
private const float duration = 500;
private Bindable<float> sizeX; private Bindable<float> sizeX;
private Bindable<float> sizeY; private Bindable<float> sizeY;
private Bindable<float> posX; private Bindable<float> posX;
@ -82,6 +84,8 @@ namespace osu.Game.Graphics.Containers
private readonly bool applyUIScale; private readonly bool applyUIScale;
private Bindable<float> uiScale; private Bindable<float> uiScale;
private float currentScale = 1;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public ScalingDrawSizePreservingFillContainer(bool applyUIScale) public ScalingDrawSizePreservingFillContainer(bool applyUIScale)
@ -95,14 +99,16 @@ namespace osu.Game.Graphics.Containers
if (applyUIScale) if (applyUIScale)
{ {
uiScale = osuConfig.GetBindable<float>(OsuSetting.UIScale); uiScale = osuConfig.GetBindable<float>(OsuSetting.UIScale);
uiScale.BindValueChanged(scaleChanged, true); uiScale.BindValueChanged(args => this.TransformTo(nameof(currentScale), args.NewValue, duration, Easing.OutQuart), true);
} }
} }
private void scaleChanged(ValueChangedEvent<float> args) protected override void Update()
{ {
this.ScaleTo(new Vector2(args.NewValue), 500, Easing.Out); Scale = new Vector2(currentScale);
this.ResizeTo(new Vector2(1 / args.NewValue), 500, Easing.Out); Size = new Vector2(1 / currentScale);
base.Update();
} }
} }
@ -140,8 +146,6 @@ namespace osu.Game.Graphics.Containers
private void updateSize() private void updateSize()
{ {
const float duration = 500;
if (targetMode == ScalingMode.Everything) if (targetMode == ScalingMode.Everything)
{ {
// the top level scaling container manages the background to be displayed while scaling. // the top level scaling container manages the background to be displayed while scaling.

View File

@ -67,7 +67,7 @@ namespace osu.Game.Online.Multiplayer
/// <summary> /// <summary>
/// Invoked when the multiplayer server requests the current beatmap to be loaded into play. /// Invoked when the multiplayer server requests the current beatmap to be loaded into play.
/// </summary> /// </summary>
public event Action? LoadRequested; public virtual event Action? LoadRequested;
/// <summary> /// <summary>
/// Invoked when the multiplayer server requests gameplay to be started. /// Invoked when the multiplayer server requests gameplay to be started.
@ -114,12 +114,12 @@ namespace osu.Game.Online.Multiplayer
/// <summary> /// <summary>
/// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available. /// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available.
/// </summary> /// </summary>
public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id); public virtual MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id);
/// <summary> /// <summary>
/// Whether the <see cref="LocalUser"/> is the host in <see cref="Room"/>. /// Whether the <see cref="LocalUser"/> is the host in <see cref="Room"/>.
/// </summary> /// </summary>
public bool IsHost public virtual bool IsHost
{ {
get get
{ {

View File

@ -25,7 +25,7 @@ namespace osu.Game.Online.Rooms
/// This differs from a regular download tracking composite as this accounts for the /// This differs from a regular download tracking composite as this accounts for the
/// databased beatmap set's checksum, to disallow from playing with an altered version of the beatmap. /// databased beatmap set's checksum, to disallow from playing with an altered version of the beatmap.
/// </summary> /// </summary>
public sealed class OnlinePlayBeatmapAvailabilityTracker : CompositeDrawable public class OnlinePlayBeatmapAvailabilityTracker : CompositeDrawable
{ {
public readonly IBindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>(); public readonly IBindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
@ -41,7 +41,7 @@ namespace osu.Game.Online.Rooms
/// <summary> /// <summary>
/// The availability state of the currently selected playlist item. /// The availability state of the currently selected playlist item.
/// </summary> /// </summary>
public IBindable<BeatmapAvailability> Availability => availability; public virtual IBindable<BeatmapAvailability> Availability => availability;
private readonly Bindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>(BeatmapAvailability.NotDownloaded()); private readonly Bindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>(BeatmapAvailability.NotDownloaded());

View File

@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
LabelText = UserInterfaceStrings.HoldToConfirmActivationTime, LabelText = UserInterfaceStrings.HoldToConfirmActivationTime,
Current = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay), Current = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay),
Keywords = new[] { @"delay" },
KeyboardStep = 50 KeyboardStep = 50
}, },
}; };

View File

@ -23,7 +23,9 @@ namespace osu.Game.Overlays.Settings
private IBindable<SettingsSection> selectedSection; private IBindable<SettingsSection> selectedSection;
private OsuSpriteText header; private Box dim;
private const float inactive_alpha = 0.8f;
public abstract Drawable CreateIcon(); public abstract Drawable CreateIcon();
public abstract LocalisableString Header { get; } public abstract LocalisableString Header { get; }
@ -78,25 +80,40 @@ namespace osu.Game.Overlays.Settings
}, },
new Container new Container
{ {
Padding = new MarginPadding Padding = new MarginPadding { Top = border_size },
{
Top = 28,
Bottom = 40,
},
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Children = new Drawable[] Children = new Drawable[]
{ {
header = new OsuSpriteText new Container
{ {
Font = OsuFont.TorusAlternate.With(size: header_size), RelativeSizeAxes = Axes.X,
Text = Header, AutoSizeAxes = Axes.Y,
Margin = new MarginPadding Padding = new MarginPadding
{ {
Horizontal = SettingsPanel.CONTENT_MARGINS Top = 24,
Bottom = 40,
},
Children = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.TorusAlternate.With(size: header_size),
Text = Header,
Margin = new MarginPadding
{
Horizontal = SettingsPanel.CONTENT_MARGINS
}
},
FlowContent
} }
}, },
FlowContent dim = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
Alpha = inactive_alpha,
},
} }
}, },
}); });
@ -134,17 +151,14 @@ namespace osu.Game.Overlays.Settings
private void updateContentFade() private void updateContentFade()
{ {
float contentFade = 1; float dimFade = 0;
float headerFade = 1;
if (!isCurrentSection) if (!isCurrentSection)
{ {
contentFade = 0.25f; dimFade = IsHovered ? 0.5f : inactive_alpha;
headerFade = IsHovered ? 0.5f : 0.25f;
} }
header.FadeTo(headerFade, 500, Easing.OutQuint); dim.FadeTo(dimFade, 300, Easing.OutQuint);
FlowContent.FadeTo(contentFade, 500, Easing.OutQuint);
} }
} }
} }

View File

@ -197,7 +197,7 @@ namespace osu.Game.Overlays
ContentContainer.Margin = new MarginPadding { Left = Sidebar?.DrawWidth ?? 0 }; ContentContainer.Margin = new MarginPadding { Left = Sidebar?.DrawWidth ?? 0 };
} }
private const double fade_in_duration = 1000; private const double fade_in_duration = 500;
private void loadSections() private void loadSections()
{ {

View File

@ -133,6 +133,11 @@ namespace osu.Game.Rulesets.UI
p.NewResult += (_, r) => NewResult?.Invoke(r); p.NewResult += (_, r) => NewResult?.Invoke(r);
p.RevertResult += (_, r) => RevertResult?.Invoke(r); p.RevertResult += (_, r) => RevertResult?.Invoke(r);
})); }));
}
protected override void LoadComplete()
{
base.LoadComplete();
IsPaused.ValueChanged += paused => IsPaused.ValueChanged += paused =>
{ {

View File

@ -25,7 +25,7 @@ namespace osu.Game.Screens.Edit.GameplayTest
} }
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
=> new MasterGameplayClockContainer(beatmap, editorState.Time, true); => new MasterGameplayClockContainer(beatmap, gameplayStart) { StartTime = editorState.Time };
protected override void LoadComplete() protected override void LoadComplete()
{ {

View File

@ -185,8 +185,7 @@ namespace osu.Game.Screens.Menu
private void load(AudioManager audio) private void load(AudioManager audio)
{ {
sampleHover = audio.Samples.Get(@"Menu/button-hover"); sampleHover = audio.Samples.Get(@"Menu/button-hover");
if (!string.IsNullOrEmpty(sampleName)) sampleClick = audio.Samples.Get(!string.IsNullOrEmpty(sampleName) ? $@"Menu/{sampleName}" : @"UI/button-select");
sampleClick = audio.Samples.Get($@"Menu/{sampleName}");
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)

View File

@ -283,9 +283,15 @@ namespace osu.Game.Screens.Menu
this.Delay(early_activation).Schedule(() => this.Delay(early_activation).Schedule(() =>
{ {
if (beatIndex % timingPoint.TimeSignature.Numerator == 0) if (beatIndex % timingPoint.TimeSignature.Numerator == 0)
sampleDownbeat.Play(); {
sampleDownbeat?.Play();
}
else else
sampleBeat.Play(); {
var channel = sampleBeat.GetChannel();
channel.Frequency.Value = 0.95 + RNG.NextDouble(0.1);
channel.Play();
}
}); });
} }

View File

@ -17,8 +17,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
Bindable<bool> WaitingOnFrames { get; } Bindable<bool> WaitingOnFrames { get; }
/// <summary> /// <summary>
/// Whether this clock is resynchronising to the master clock. /// Whether this clock is behind the master clock and running at a higher rate to catch up to it.
/// </summary> /// </summary>
/// <remarks>
/// Of note, this will be false if this clock is *ahead* of the master clock.
/// </remarks>
bool IsCatchingUp { get; set; } bool IsCatchingUp { get; set; }
/// <summary> /// <summary>

View File

@ -55,12 +55,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public SpectatorGameplayClockContainer([NotNull] IClock sourceClock) public SpectatorGameplayClockContainer([NotNull] IClock sourceClock)
: base(sourceClock) : base(sourceClock)
{ {
// the container should initially be in a stopped state until the catch-up clock is started by the sync manager.
Stop();
} }
protected override void Update() protected override void Update()
{ {
// The SourceClock here is always a CatchUpSpectatorPlayerClock.
// The player clock's running state is controlled externally, but the local pausing state needs to be updated to stop gameplay. // The player clock's running state is controlled externally, but the local pausing state needs to be updated to stop gameplay.
if (SourceClock.IsRunning) if (SourceClock.IsRunning)
Start(); Start();

View File

@ -164,7 +164,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
base.LoadComplete(); base.LoadComplete();
masterClockContainer.Reset(); masterClockContainer.Reset();
masterClockContainer.Stop();
syncManager.ReadyToStart += onReadyToStart; syncManager.ReadyToStart += onReadyToStart;
syncManager.MasterState.BindValueChanged(onMasterStateChanged, true); syncManager.MasterState.BindValueChanged(onMasterStateChanged, true);
@ -198,8 +197,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
.DefaultIfEmpty(0) .DefaultIfEmpty(0)
.Min(); .Min();
masterClockContainer.Seek(startTime); masterClockContainer.StartTime = startTime;
masterClockContainer.Start(); masterClockContainer.Reset(true);
// Although the clock has been started, this flag is set to allow for later synchronisation state changes to also be able to start it. // Although the clock has been started, this flag is set to allow for later synchronisation state changes to also be able to start it.
canStartMasterClock = true; canStartMasterClock = true;

View File

@ -24,7 +24,7 @@ namespace osu.Game.Screens.Play
/// <summary> /// <summary>
/// Whether gameplay is paused. /// Whether gameplay is paused.
/// </summary> /// </summary>
public readonly BindableBool IsPaused = new BindableBool(); public readonly BindableBool IsPaused = new BindableBool(true);
/// <summary> /// <summary>
/// The adjustable source clock used for gameplay. Should be used for seeks and clock control. /// The adjustable source clock used for gameplay. Should be used for seeks and clock control.
@ -41,6 +41,15 @@ namespace osu.Game.Screens.Play
/// </summary> /// </summary>
public event Action OnSeek; public event Action OnSeek;
/// <summary>
/// The time from which the clock should start. Will be seeked to on calling <see cref="Reset"/>.
/// </summary>
/// <remarks>
/// If not set, a value of zero will be used.
/// Importantly, the value will be inferred from the current ruleset in <see cref="MasterGameplayClockContainer"/> unless specified.
/// </remarks>
public double? StartTime { get; set; }
/// <summary> /// <summary>
/// Creates a new <see cref="GameplayClockContainer"/>. /// Creates a new <see cref="GameplayClockContainer"/>.
/// </summary> /// </summary>
@ -106,16 +115,17 @@ namespace osu.Game.Screens.Play
/// <summary> /// <summary>
/// Resets this <see cref="GameplayClockContainer"/> and the source to an initial state ready for gameplay. /// Resets this <see cref="GameplayClockContainer"/> and the source to an initial state ready for gameplay.
/// </summary> /// </summary>
public virtual void Reset() /// <param name="startClock">Whether to start the clock immediately, if not already started.</param>
public void Reset(bool startClock = false)
{ {
ensureSourceClockSet();
Seek(0);
// Manually stop the source in order to not affect the IsPaused state. // Manually stop the source in order to not affect the IsPaused state.
AdjustableSource.Stop(); AdjustableSource.Stop();
if (!IsPaused.Value) if (!IsPaused.Value || startClock)
Start(); Start();
ensureSourceClockSet();
Seek(StartTime ?? 0);
} }
/// <summary> /// <summary>

View File

@ -46,36 +46,36 @@ namespace osu.Game.Screens.Play
private double totalAppliedOffset => userBeatmapOffsetClock.RateAdjustedOffset + userGlobalOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; private double totalAppliedOffset => userBeatmapOffsetClock.RateAdjustedOffset + userGlobalOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset;
private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); private readonly BindableDouble pauseFreqAdjust = new BindableDouble(); // Important that this starts at zero, matching the paused state of the clock.
private readonly WorkingBeatmap beatmap; private readonly WorkingBeatmap beatmap;
private readonly double gameplayStartTime;
private readonly bool startAtGameplayStart;
private readonly double firstHitObjectTime;
private HardwareCorrectionOffsetClock userGlobalOffsetClock; private HardwareCorrectionOffsetClock userGlobalOffsetClock;
private HardwareCorrectionOffsetClock userBeatmapOffsetClock; private HardwareCorrectionOffsetClock userBeatmapOffsetClock;
private HardwareCorrectionOffsetClock platformOffsetClock; private HardwareCorrectionOffsetClock platformOffsetClock;
private MasterGameplayClock masterGameplayClock; private MasterGameplayClock masterGameplayClock;
private Bindable<double> userAudioOffset; private Bindable<double> userAudioOffset;
private double startOffset;
private IDisposable beatmapOffsetSubscription; private IDisposable beatmapOffsetSubscription;
private readonly double skipTargetTime;
[Resolved] [Resolved]
private RealmAccess realm { get; set; } private RealmAccess realm { get; set; }
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) /// <summary>
/// Create a new master gameplay clock container.
/// </summary>
/// <param name="beatmap">The beatmap to be used for time and metadata references.</param>
/// <param name="skipTargetTime">The latest time which should be used when introducing gameplay. Will be used when skipping forward.</param>
public MasterGameplayClockContainer(WorkingBeatmap beatmap, double skipTargetTime)
: base(beatmap.Track) : base(beatmap.Track)
{ {
this.beatmap = beatmap; this.beatmap = beatmap;
this.gameplayStartTime = gameplayStartTime; this.skipTargetTime = skipTargetTime;
this.startAtGameplayStart = startAtGameplayStart;
firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -90,41 +90,67 @@ namespace osu.Game.Screens.Play
settings => settings.Offset, settings => settings.Offset,
val => userBeatmapOffsetClock.Offset = val); val => userBeatmapOffsetClock.Offset = val);
// sane default provided by ruleset. // Reset may have been called externally before LoadComplete.
startOffset = gameplayStartTime; // If it was, and the clock is in a playing state, we want to ensure that it isn't stopped here.
bool isStarted = !IsPaused.Value;
if (!startAtGameplayStart) // If a custom start time was not specified, calculate the best value to use.
{ StartTime ??= findEarliestStartTime();
startOffset = Math.Min(0, startOffset);
// if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. Reset(startClock: isStarted);
// this is commonly used to display an intro before the audio track start. }
double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime;
if (firstStoryboardEvent != null)
startOffset = Math.Min(startOffset, firstStoryboardEvent.Value);
// some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. private double findEarliestStartTime()
// this is not available as an option in the live editor but can still be applied via .osu editing. {
if (beatmap.BeatmapInfo.AudioLeadIn > 0) // here we are trying to find the time to start playback from the "zero" point.
startOffset = Math.Min(startOffset, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); // generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc.
}
Seek(startOffset); // start with the originally provided latest time (if before zero).
double time = Math.Min(0, skipTargetTime);
// if a storyboard is present, it may dictate the appropriate start time by having events in negative time space.
// this is commonly used to display an intro before the audio track start.
double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime;
if (firstStoryboardEvent != null)
time = Math.Min(time, firstStoryboardEvent.Value);
// some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available.
// this is not available as an option in the live editor but can still be applied via .osu editing.
double firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime;
if (beatmap.BeatmapInfo.AudioLeadIn > 0)
time = Math.Min(time, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn);
return time;
} }
protected override void OnIsPausedChanged(ValueChangedEvent<bool> isPaused) protected override void OnIsPausedChanged(ValueChangedEvent<bool> isPaused)
{ {
// The source is stopped by a frequency fade first. if (IsLoaded)
if (isPaused.NewValue)
{ {
this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => // During normal operation, the source is stopped after performing a frequency ramp.
if (isPaused.NewValue)
{ {
if (IsPaused.Value == isPaused.NewValue) this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ =>
AdjustableSource.Stop(); {
}); if (IsPaused.Value == isPaused.NewValue)
AdjustableSource.Stop();
});
}
else
this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In);
} }
else else
this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); {
if (isPaused.NewValue)
AdjustableSource.Stop();
// If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations.
pauseFreqAdjust.Value = isPaused.NewValue ? 0 : 1;
// We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment.
// Without doing this, an initial seek may be performed with the wrong offset.
GameplayClock.UnderlyingClock.ProcessFrame();
}
} }
public override void Start() public override void Start()
@ -152,10 +178,10 @@ namespace osu.Game.Screens.Play
/// </summary> /// </summary>
public void Skip() public void Skip()
{ {
if (GameplayClock.CurrentTime > gameplayStartTime - MINIMUM_SKIP_TIME) if (GameplayClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME)
return; return;
double skipTarget = gameplayStartTime - MINIMUM_SKIP_TIME; double skipTarget = skipTargetTime - MINIMUM_SKIP_TIME;
if (GameplayClock.CurrentTime < 0 && skipTarget > 6000) if (GameplayClock.CurrentTime < 0 && skipTarget > 6000)
// double skip exception for storyboards with very long intros // double skip exception for storyboards with very long intros
@ -164,12 +190,6 @@ namespace osu.Game.Screens.Play
Seek(skipTarget); Seek(skipTarget);
} }
public override void Reset()
{
base.Reset();
Seek(startOffset);
}
protected override GameplayClock CreateGameplayClock(IFrameBasedClock source) protected override GameplayClock CreateGameplayClock(IFrameBasedClock source)
{ {
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
@ -278,7 +298,6 @@ namespace osu.Game.Screens.Play
private class MasterGameplayClock : GameplayClock private class MasterGameplayClock : GameplayClock
{ {
public readonly List<Bindable<double>> MutableNonGameplayAdjustments = new List<Bindable<double>>(); public readonly List<Bindable<double>> MutableNonGameplayAdjustments = new List<Bindable<double>>();
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => MutableNonGameplayAdjustments; public override IEnumerable<Bindable<double>> NonGameplayAdjustments => MutableNonGameplayAdjustments;
public MasterGameplayClock(FramedOffsetClock underlyingClock) public MasterGameplayClock(FramedOffsetClock underlyingClock)

View File

@ -607,30 +607,25 @@ namespace osu.Game.Screens.Play
private ScheduledDelegate frameStablePlaybackResetDelegate; private ScheduledDelegate frameStablePlaybackResetDelegate;
/// <summary> /// <summary>
/// Seeks to a specific time in gameplay, bypassing frame stability. /// Specify and seek to a custom start time from which gameplay should be observed.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Intermediate hitobject judgements may not be applied or reverted correctly during this seek. /// This performs a non-frame-stable seek. Intermediate hitobject judgements may not be applied or reverted correctly during this seek.
/// </remarks> /// </remarks>
/// <param name="time">The destination time to seek to.</param> /// <param name="time">The destination time to seek to.</param>
internal void NonFrameStableSeek(double time) protected void SetGameplayStartTime(double time)
{ {
// TODO: This schedule should not be required and is a temporary hotfix. if (frameStablePlaybackResetDelegate?.Cancelled == false && !frameStablePlaybackResetDelegate.Completed)
// See https://github.com/ppy/osu/issues/17267 for the issue. frameStablePlaybackResetDelegate.RunTask();
// See https://github.com/ppy/osu/pull/17302 for a better fix which needs some more time.
ScheduleAfterChildren(() =>
{
if (frameStablePlaybackResetDelegate?.Cancelled == false && !frameStablePlaybackResetDelegate.Completed)
frameStablePlaybackResetDelegate.RunTask();
bool wasFrameStable = DrawableRuleset.FrameStablePlayback; bool wasFrameStable = DrawableRuleset.FrameStablePlayback;
DrawableRuleset.FrameStablePlayback = false; DrawableRuleset.FrameStablePlayback = false;
Seek(time); GameplayClockContainer.StartTime = time;
GameplayClockContainer.Reset();
// Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek. // Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek.
frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable); frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable);
});
} }
/// <summary> /// <summary>
@ -987,7 +982,7 @@ namespace osu.Game.Screens.Play
if (GameplayClockContainer.GameplayClock.IsRunning) if (GameplayClockContainer.GameplayClock.IsRunning)
throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"); throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running");
GameplayClockContainer.Reset(); GameplayClockContainer.Reset(true);
} }
public override void OnSuspending(IScreen next) public override void OnSuspending(IScreen next)

View File

@ -78,7 +78,7 @@ namespace osu.Game.Screens.Play
} }
if (isFirstBundle && score.Replay.Frames.Count > 0) if (isFirstBundle && score.Replay.Frames.Count > 0)
NonFrameStableSeek(score.Replay.Frames[0].Time); SetGameplayStartTime(score.Replay.Frames[0].Time);
} }
protected override Score CreateScore(IBeatmap beatmap) => score; protected override Score CreateScore(IBeatmap beatmap) => score;

View File

@ -7,19 +7,16 @@ 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.API.Requests.Responses;
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;
using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@ -141,16 +138,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
switch (Room.State) switch (Room.State)
{ {
case MultiplayerRoomState.Open: case MultiplayerRoomState.Open:
// If there are no remaining ready users or the host is not ready, stop any existing countdown.
// Todo: This doesn't yet support non-match-start countdowns.
if (Room.Settings.AutoStartEnabled)
{
bool shouldHaveCountdown = !APIRoom.Playlist.GetCurrentItem()!.Expired && Room.Users.Any(u => u.State == MultiplayerUserState.Ready);
if (shouldHaveCountdown && Room.Countdown == null)
startCountdown(new MatchStartCountdown { TimeRemaining = Room.Settings.AutoStartDuration }, StartMatch);
}
break; break;
case MultiplayerRoomState.WaitingForLoad: case MultiplayerRoomState.WaitingForLoad:
@ -317,16 +304,6 @@ 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);
@ -334,14 +311,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
switch (request) switch (request)
{ {
case StartMatchCountdownRequest matchCountdownRequest:
startCountdown(new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration }, StartMatch);
break;
case StopCountdownRequest _:
stopCountdown();
break;
case ChangeTeamRequest changeTeam: case ChangeTeamRequest changeTeam:
TeamVersusRoomState roomState = (TeamVersusRoomState)Room.MatchState!; TeamVersusRoomState roomState = (TeamVersusRoomState)Room.MatchState!;
@ -360,62 +329,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
} }
private void startCountdown(MultiplayerCountdown countdown, Func<Task> continuation)
{
Debug.Assert(Room != null);
Debug.Assert(ThreadSafety.IsUpdateThread);
stopCountdown();
// 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();
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(countdown.TimeRemaining, 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;
continuation().WaitSafely();
});
}
}
private void stopCountdown() => countdownStopSource?.Cancel();
public override Task StartMatch() public override Task StartMatch()
{ {
Debug.Assert(Room != null); Debug.Assert(Room != null);

View File

@ -35,8 +35,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.10.0" /> <PackageReference Include="Realm" Version="10.10.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.408.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.415.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.407.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.415.0" />
<PackageReference Include="Sentry" Version="3.14.1" /> <PackageReference Include="Sentry" Version="3.14.1" />
<PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="SharpCompress" Version="0.30.1" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -61,8 +61,8 @@
<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="2022.408.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.415.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.407.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.415.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
<PropertyGroup> <PropertyGroup>
@ -84,7 +84,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.408.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.415.0" />
<PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="SharpCompress" Version="0.30.1" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />