mirror of
https://github.com/ppy/osu.git
synced 2024-12-13 08:32:57 +08:00
Merge branch 'master' into zh-hant
This commit is contained in:
commit
7781a4250e
@ -9,7 +9,7 @@
|
||||
]
|
||||
},
|
||||
"jetbrains.resharper.globaltools": {
|
||||
"version": "2020.3.2",
|
||||
"version": "2022.1.0-eap10",
|
||||
"commands": [
|
||||
"jb"
|
||||
]
|
||||
@ -27,7 +27,7 @@
|
||||
]
|
||||
},
|
||||
"ppy.localisationanalyser.tools": {
|
||||
"version": "2022.320.0",
|
||||
"version": "2022.417.0",
|
||||
"commands": [
|
||||
"localisation"
|
||||
]
|
||||
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@ -120,6 +120,12 @@ jobs:
|
||||
- name: Restore Packages
|
||||
run: dotnet restore
|
||||
|
||||
- name: Restore inspectcode cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/inspectcode
|
||||
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json') }}-${{ hashFiles('.github/workflows/ci.yml' ) }}
|
||||
|
||||
- name: CodeFileSanity
|
||||
run: |
|
||||
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
|
||||
@ -138,7 +144,7 @@ jobs:
|
||||
# run: dotnet format --dry-run --check
|
||||
|
||||
- name: InspectCode
|
||||
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN
|
||||
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
|
||||
|
||||
- name: NVika
|
||||
run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
|
||||
|
@ -1,5 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SwUserDefinedSpecifications">
|
||||
<option name="specTypeByUrl">
|
||||
<map />
|
||||
</option>
|
||||
</component>
|
||||
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
|
||||
<option name="ENSURE_MISC_FILE_EXISTS" value="true" />
|
||||
</component>
|
||||
|
@ -5,7 +5,7 @@ dotnet tool restore
|
||||
# - cmd: dotnet format --dry-run --check
|
||||
|
||||
dotnet CodeFileSanity
|
||||
dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
|
||||
dotnet jb inspectcode "osu.Desktop.slnf" --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
|
||||
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
|
||||
|
||||
exit $LASTEXITCODE
|
||||
|
@ -2,5 +2,5 @@
|
||||
|
||||
dotnet tool restore
|
||||
dotnet CodeFileSanity
|
||||
dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
|
||||
dotnet jb inspectcode "osu.Desktop.slnf" --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
|
||||
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.EmptyFreeform
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.Pippidon
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.EmptyScrolling
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.Pippidon
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
||||
}
|
||||
}
|
||||
|
@ -51,8 +51,8 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.407.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.408.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.415.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.415.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
|
@ -28,9 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
|
||||
// flying hits all land in one common scrolling container (and stay there for rewind purposes),
|
||||
// so we need to manually get the latest one.
|
||||
flyingHit = this.ChildrenOfType<DrawableFlyingHit>()
|
||||
.OrderByDescending(h => h.HitObject.StartTime)
|
||||
.FirstOrDefault();
|
||||
flyingHit = this.ChildrenOfType<DrawableFlyingHit>().MaxBy(h => h.HitObject.StartTime);
|
||||
});
|
||||
|
||||
AddAssert("hit type is correct", () => flyingHit.HitObject.Type == hitType);
|
||||
|
@ -26,6 +26,12 @@ namespace osu.Game.Tests.Gameplay
|
||||
Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("reset audio offset", () => localConfig.SetValue(OsuSetting.AudioOffset, 0.0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStartThenElapsedTime()
|
||||
{
|
||||
@ -36,7 +42,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
working.LoadTrack();
|
||||
|
||||
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
|
||||
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
|
||||
});
|
||||
|
||||
AddStep("start clock", () => gameplayClockContainer.Start());
|
||||
@ -53,7 +59,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
working.LoadTrack();
|
||||
|
||||
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
|
||||
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
|
||||
});
|
||||
|
||||
AddStep("start clock", () => gameplayClockContainer.Start());
|
||||
@ -73,26 +79,29 @@ namespace osu.Game.Tests.Gameplay
|
||||
public void TestSeekPerformsInGameplayTime(
|
||||
[Values(1.0, 0.5, 2.0)] double clockRate,
|
||||
[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;
|
||||
GameplayClockContainer gameplayClockContainer = null;
|
||||
|
||||
if (setAudioOffsetBeforeConstruction)
|
||||
AddStep($"preset audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
|
||||
|
||||
AddStep("create container", () =>
|
||||
{
|
||||
working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio);
|
||||
working.LoadTrack();
|
||||
|
||||
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
|
||||
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
|
||||
|
||||
if (whileStopped)
|
||||
gameplayClockContainer.Stop();
|
||||
|
||||
gameplayClockContainer.Reset();
|
||||
gameplayClockContainer.Reset(startClock: !whileStopped);
|
||||
});
|
||||
|
||||
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));
|
||||
AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f));
|
||||
|
@ -88,7 +88,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
[Test]
|
||||
public void TestSampleHasLifetimeEndWithInitialClockTime()
|
||||
{
|
||||
GameplayClockContainer gameplayContainer = null;
|
||||
MasterGameplayClockContainer gameplayContainer = null;
|
||||
DrawableStoryboardSample sample = null;
|
||||
|
||||
AddStep("create container", () =>
|
||||
@ -96,8 +96,11 @@ namespace osu.Game.Tests.Gameplay
|
||||
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
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 },
|
||||
Child = new FrameStabilityContainer
|
||||
{
|
||||
|
@ -33,9 +33,10 @@ namespace osu.Game.Tests.Online
|
||||
|
||||
var converted = deserialized?.ToMod(new TestRuleset());
|
||||
|
||||
Assert.NotNull(converted);
|
||||
Assert.That(converted, Is.TypeOf(typeof(UnknownMod)));
|
||||
Assert.That(converted?.Type, Is.EqualTo(ModType.System));
|
||||
Assert.That(converted?.Acronym, Is.EqualTo("WNG??"));
|
||||
Assert.That(converted.Type, Is.EqualTo(ModType.System));
|
||||
Assert.That(converted.Acronym, Is.EqualTo("WNG??"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -56,10 +56,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
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;
|
||||
|
||||
FrequencyIncreased |= freq > lastFrequency;
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddUntilStep("get variables", () =>
|
||||
{
|
||||
sampleDisabler = Player;
|
||||
slider = Player.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).FirstOrDefault();
|
||||
slider = Player.ChildrenOfType<DrawableSlider>().MinBy(s => s.HitObject.StartTime);
|
||||
samples = slider?.ChildrenOfType<PoolableSkinnableSample>().ToArray();
|
||||
|
||||
return slider != null;
|
||||
|
@ -1,12 +1,10 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@ -36,10 +34,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
BeatmapInfo = { AudioLeadIn = leadIn }
|
||||
});
|
||||
|
||||
AddAssert($"first frame is {expectedStartTime}", () =>
|
||||
AddStep("check first frame time", () =>
|
||||
{
|
||||
Debug.Assert(player.FirstFrameClockTime != null);
|
||||
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms);
|
||||
Assert.That(player.FirstFrameClockTime, Is.Not.Null);
|
||||
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);
|
||||
|
||||
AddAssert($"first frame is {expectedStartTime}", () =>
|
||||
AddStep("check first frame time", () =>
|
||||
{
|
||||
Debug.Assert(player.FirstFrameClockTime != null);
|
||||
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms);
|
||||
Assert.That(player.FirstFrameClockTime, Is.Not.Null);
|
||||
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);
|
||||
|
||||
AddAssert($"first frame is {expectedStartTime}", () =>
|
||||
AddStep("check first frame time", () =>
|
||||
{
|
||||
Debug.Assert(player.FirstFrameClockTime != null);
|
||||
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms);
|
||||
Assert.That(player.FirstFrameClockTime, Is.Not.Null);
|
||||
Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[TestFixture]
|
||||
public class TestSceneReplayDownloadButton : OsuManualInputManagerTestScene
|
||||
{
|
||||
private const long online_score_id = 2553163309;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
@ -43,6 +45,15 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("delete previous imports", () =>
|
||||
{
|
||||
scoreManager.Delete(s => s.OnlineID == online_score_id);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisplayStates()
|
||||
{
|
||||
@ -180,7 +191,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
return new APIScore
|
||||
{
|
||||
OnlineID = 2553163309,
|
||||
OnlineID = online_score_id,
|
||||
RulesetID = 0,
|
||||
Beatmap = CreateAPIBeatmapSet(new OsuRuleset().RulesetInfo).Beatmaps.First(),
|
||||
HasReplay = replayAvailable,
|
||||
|
@ -27,6 +27,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public abstract class MultiplayerGameplayLeaderboardTestScene : OsuTestScene
|
||||
{
|
||||
private const int total_users = 16;
|
||||
|
||||
protected readonly BindableList<MultiplayerRoomUser> MultiplayerUsers = new BindableList<MultiplayerRoomUser>();
|
||||
|
||||
protected MultiplayerGameplayLeaderboard Leaderboard { get; private set; }
|
||||
@ -84,7 +86,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[SetUpSteps]
|
||||
public virtual void SetUpSteps()
|
||||
{
|
||||
AddStep("reset counts", () => spectatorClient.Invocations.Clear());
|
||||
AddStep("reset counts", () =>
|
||||
{
|
||||
spectatorClient.Invocations.Clear();
|
||||
lastHeaders.Clear();
|
||||
});
|
||||
|
||||
AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = new APIUser
|
||||
{
|
||||
@ -94,7 +100,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("populate users", () =>
|
||||
{
|
||||
MultiplayerUsers.Clear();
|
||||
for (int i = 0; i < 16; i++)
|
||||
for (int i = 0; i < total_users; i++)
|
||||
MultiplayerUsers.Add(CreateUser(i));
|
||||
});
|
||||
|
||||
|
@ -9,13 +9,14 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public class TestSceneGameplayChatDisplay : MultiplayerTestScene
|
||||
public class TestSceneGameplayChatDisplay : OsuManualInputManagerTestScene
|
||||
{
|
||||
private GameplayChatDisplay chatDisplay;
|
||||
|
||||
@ -35,11 +36,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
public void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("load chat display", () => Child = chatDisplay = new GameplayChatDisplay(SelectedRoom.Value)
|
||||
AddStep("load chat display", () => Child = chatDisplay = new GameplayChatDisplay(new Room())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
|
@ -3,80 +3,155 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
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.Logging;
|
||||
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;
|
||||
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
|
||||
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 BeatmapSetInfo importedSet;
|
||||
|
||||
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
||||
private OsuButton readyButton => control.ChildrenOfType<OsuButton>().Single();
|
||||
|
||||
private BeatmapManager beatmaps;
|
||||
private RulesetStore rulesets;
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
|
||||
new CachedModelDependencyContainer<Room>(base.CreateChildDependencies(parent)) { Model = { BindTarget = room } };
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
private void load()
|
||||
{
|
||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(Realm);
|
||||
Dependencies.CacheAs(multiplayerClient.Object);
|
||||
Dependencies.CacheAs(ongoingOperationTracker = new OngoingOperationTracker());
|
||||
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]
|
||||
public new void Setup() => Schedule(() =>
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
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)
|
||||
AddStep("reset state", () =>
|
||||
{
|
||||
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,
|
||||
Child = control = new MatchStartControl
|
||||
content.Child = control = new MatchStartControl
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(250, 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", () =>
|
||||
{
|
||||
@ -85,8 +160,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
|
||||
AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad);
|
||||
AddStep("check request received", () =>
|
||||
{
|
||||
multiplayerClient.Verify(m => m.SendMatchRequest(It.Is<StartMatchCountdownRequest>(req =>
|
||||
req.Duration == TimeSpan.FromSeconds(10)
|
||||
)), Times.Once);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -94,6 +173,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||
AddStep("click the first countdown button", () =>
|
||||
{
|
||||
@ -102,6 +182,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
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>();
|
||||
AddStep("click the cancel button", () =>
|
||||
{
|
||||
@ -110,41 +197,39 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
|
||||
AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
|
||||
AddStep("check request received", () =>
|
||||
{
|
||||
multiplayerClient.Verify(m => m.SendMatchRequest(It.IsAny<StopCountdownRequest>()), Times.Once);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReadyAndUnReadyDuringCountdown()
|
||||
{
|
||||
AddStep("add second user as host", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||
MultiplayerClient.TransferHost(2);
|
||||
});
|
||||
AddStep("add second user as host", () => addUser(new APIUser { Id = 2, Username = "Another user" }, true));
|
||||
|
||||
AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(2) }).WaitSafely());
|
||||
AddStep("start countdown", () => setRoomCountdown(TimeSpan.FromMinutes(1)));
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||
checkLocalUserState(MultiplayerUserState.Ready);
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
||||
checkLocalUserState(MultiplayerUserState.Idle);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCountdownWhileSpectating()
|
||||
{
|
||||
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
|
||||
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
||||
AddStep("set spectating", () => changeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
|
||||
checkLocalUserState(MultiplayerUserState.Spectating);
|
||||
|
||||
AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -153,60 +238,54 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add second user as host", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||
MultiplayerClient.TransferHost(2);
|
||||
addUser(new APIUser { Id = 2, Username = "Another user" }, true);
|
||||
});
|
||||
|
||||
AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely());
|
||||
AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null);
|
||||
AddStep("start countdown", () => multiplayerClient.Object.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely());
|
||||
AddUntilStep("countdown started", () => multiplayerRoom.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);
|
||||
AddStep("transfer host to local user", () => transferHost(localUser));
|
||||
AddUntilStep("local user is host", () => multiplayerRoom.Host?.Equals(multiplayerClient.Object.LocalUser) == true);
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
|
||||
AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null);
|
||||
checkLocalUserState(MultiplayerUserState.Ready);
|
||||
AddAssert("countdown still active", () => multiplayerRoom.Countdown != null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCountdownButtonVisibilityWithAutoStartEnablement()
|
||||
public void TestCountdownButtonVisibilityWithAutoStart()
|
||||
{
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
|
||||
checkLocalUserState(MultiplayerUserState.Ready);
|
||||
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>();
|
||||
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
|
||||
checkLocalUserState(MultiplayerUserState.Ready);
|
||||
AddUntilStep("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
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>();
|
||||
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
|
||||
checkLocalUserState(MultiplayerUserState.Ready);
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("local user became idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle);
|
||||
checkLocalUserState(MultiplayerUserState.Idle);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDeletedBeatmapDisableReady()
|
||||
{
|
||||
OsuButton readyButton = null;
|
||||
AddUntilStep("ready button enabled", () => readyButton.Enabled.Value);
|
||||
|
||||
AddUntilStep("ensure ready button enabled", () =>
|
||||
{
|
||||
readyButton = control.ChildrenOfType<OsuButton>().Single();
|
||||
return readyButton.Enabled.Value;
|
||||
});
|
||||
|
||||
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
|
||||
AddStep("mark beatmap not available", () => beatmapAvailability.Value = BeatmapAvailability.NotDownloaded());
|
||||
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);
|
||||
}
|
||||
|
||||
@ -215,31 +294,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add second user as host", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||
MultiplayerClient.TransferHost(2);
|
||||
addUser(new APIUser { Id = 2, Username = "Another user" }, true);
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||
checkLocalUserState(MultiplayerUserState.Ready);
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
||||
checkLocalUserState(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" });
|
||||
});
|
||||
if (!allReady)
|
||||
AddStep("add other user", () => addUser(new APIUser { Id = 2, Username = "Another user" }));
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||
checkLocalUserState(MultiplayerUserState.Ready);
|
||||
|
||||
verifyGameplayStartFlow();
|
||||
}
|
||||
@ -249,12 +322,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add host", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||
MultiplayerClient.TransferHost(2);
|
||||
addUser(new APIUser { Id = 2, Username = "Another user" }, true);
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
|
||||
|
||||
AddStep("make local user host", () => transferHost(localUser));
|
||||
|
||||
verifyGameplayStartFlow();
|
||||
}
|
||||
@ -264,18 +337,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("setup", () =>
|
||||
{
|
||||
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
|
||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||
addUser(new APIUser { Id = 2, Username = "Another user" });
|
||||
});
|
||||
|
||||
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>();
|
||||
AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
||||
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
||||
checkLocalUserState(MultiplayerUserState.Idle);
|
||||
AddUntilStep("ready button enabled", () => readyButton.Enabled.Value);
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
@ -283,44 +355,83 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
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));
|
||||
AddStep("add many users", () =>
|
||||
{
|
||||
for (int i = 0; i < users; i++)
|
||||
addUser(new APIUser { Id = i, Username = "Another user" }, !isHost && i == 2);
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
|
||||
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);
|
||||
|
||||
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)
|
||||
MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
|
||||
changeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
|
||||
}, users);
|
||||
}
|
||||
|
||||
private void verifyGameplayStartFlow()
|
||||
{
|
||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||
checkLocalUserState(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);
|
||||
AddStep("check start request received", () => multiplayerClient.Verify(m => m.StartMatch(), Times.Once));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -464,16 +464,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
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)
|
||||
{
|
||||
this.gameplayStartTime = gameplayStartTime;
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
protected override MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap)
|
||||
=> new MasterGameplayClockContainer(beatmap, gameplayStartTime ?? 0, gameplayStartTime.HasValue);
|
||||
=> new MasterGameplayClockContainer(beatmap, 0) { StartTime = startTime ?? 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -495,17 +495,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
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() });
|
||||
|
||||
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());
|
||||
|
||||
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]
|
||||
@ -665,6 +668,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
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]
|
||||
public void TestRoomSettingsReQueriedWhenJoiningRoom()
|
||||
{
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
@ -14,12 +13,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public class TestSceneMultiplayerGameplayLeaderboardTeams : MultiplayerGameplayLeaderboardTestScene
|
||||
{
|
||||
private int team;
|
||||
|
||||
protected override MultiplayerRoomUser CreateUser(int userId)
|
||||
{
|
||||
var user = base.CreateUser(userId);
|
||||
user.MatchState = new TeamVersusUserState
|
||||
{
|
||||
TeamID = RNG.Next(0, 2)
|
||||
TeamID = team++ % 2
|
||||
};
|
||||
return user;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
@ -13,13 +14,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[SetUp]
|
||||
public new void Setup() => Schedule(() =>
|
||||
{
|
||||
Child = new Container
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 50,
|
||||
Child = new MultiplayerMatchFooter()
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 50,
|
||||
Child = new MultiplayerMatchFooter()
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -26,10 +26,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
|
||||
var score = TestResources.CreateTestScoreInfo(beatmapInfo);
|
||||
|
||||
SortedDictionary<int, BindableInt> teamScores = new SortedDictionary<int, BindableInt>
|
||||
SortedDictionary<int, BindableLong> teamScores = new SortedDictionary<int, BindableLong>
|
||||
{
|
||||
{ 0, new BindableInt(team1Score) },
|
||||
{ 1, new BindableInt(team2Score) }
|
||||
{ 0, new BindableLong(team1Score) },
|
||||
{ 1, new BindableLong(team2Score) }
|
||||
};
|
||||
|
||||
Stack.Push(screen = new MultiplayerTeamResultsScreen(score, 1, new PlaylistItem(beatmapInfo), teamScores));
|
||||
|
@ -1,67 +1,68 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public class TestSceneRankRangePill : MultiplayerTestScene
|
||||
public class TestSceneRankRangePill : OsuTestScene
|
||||
{
|
||||
[SetUp]
|
||||
public new void Setup() => Schedule(() =>
|
||||
private readonly Mock<MultiplayerClient> multiplayerClient = new Mock<MultiplayerClient>();
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
|
||||
// not used directly in component, but required due to it inheriting from OnlinePlayComposite.
|
||||
new CachedModelDependencyContainer<Room>(base.CreateChildDependencies(parent));
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Dependencies.CacheAs(multiplayerClient.Object);
|
||||
|
||||
Child = new RankRangePill
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleUser()
|
||||
{
|
||||
AddStep("add user", () =>
|
||||
setupRoomWithUsers(new APIUser
|
||||
{
|
||||
MultiplayerClient.AddUser(new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Statistics = { GlobalRank = 1234 }
|
||||
});
|
||||
|
||||
// Remove the local user so only the one above is displayed.
|
||||
MultiplayerClient.RemoveUser(API.LocalUser.Value);
|
||||
Id = 2,
|
||||
Statistics = { GlobalRank = 1234 }
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleUsers()
|
||||
{
|
||||
AddStep("add users", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new APIUser
|
||||
setupRoomWithUsers(
|
||||
new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Statistics = { GlobalRank = 1234 }
|
||||
});
|
||||
|
||||
MultiplayerClient.AddUser(new APIUser
|
||||
},
|
||||
new APIUser
|
||||
{
|
||||
Id = 3,
|
||||
Statistics = { GlobalRank = 3333 }
|
||||
});
|
||||
|
||||
MultiplayerClient.AddUser(new APIUser
|
||||
},
|
||||
new APIUser
|
||||
{
|
||||
Id = 4,
|
||||
Statistics = { GlobalRank = 4321 }
|
||||
});
|
||||
|
||||
// Remove the local user so only the ones above are displayed.
|
||||
MultiplayerClient.RemoveUser(API.LocalUser.Value);
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(1, 10)]
|
||||
@ -73,22 +74,29 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[TestCase(1000000, 10000000)]
|
||||
public void TestRange(int min, int max)
|
||||
{
|
||||
AddStep("add users", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new APIUser
|
||||
setupRoomWithUsers(
|
||||
new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Statistics = { GlobalRank = min }
|
||||
});
|
||||
|
||||
MultiplayerClient.AddUser(new APIUser
|
||||
},
|
||||
new APIUser
|
||||
{
|
||||
Id = 3,
|
||||
Statistics = { GlobalRank = max }
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the local user so only the ones above are displayed.
|
||||
MultiplayerClient.RemoveUser(API.LocalUser.Value);
|
||||
private void setupRoomWithUsers(params APIUser[] users)
|
||||
{
|
||||
AddStep("setup room", () =>
|
||||
{
|
||||
multiplayerClient.SetupGet(m => m.Room).Returns(new MultiplayerRoom(0)
|
||||
{
|
||||
Users = new List<MultiplayerRoomUser>(users.Select(apiUser => new MultiplayerRoomUser(apiUser.Id) { User = apiUser }))
|
||||
});
|
||||
|
||||
multiplayerClient.Raise(m => m.RoomUpdated -= null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -171,6 +171,46 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public void TestPerformBlockedByDialogSubScreen(bool confirm)
|
||||
{
|
||||
TestScreenWithNestedStack screenWithNestedStack = null;
|
||||
|
||||
PushAndConfirm(() => screenWithNestedStack = new TestScreenWithNestedStack());
|
||||
|
||||
AddAssert("wait for nested screen", () => screenWithNestedStack.SubScreenStack.CurrentScreen == screenWithNestedStack.Blocker);
|
||||
|
||||
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
|
||||
|
||||
AddUntilStep("wait for dialog", () => screenWithNestedStack.Blocker.ExitAttempts == 1);
|
||||
|
||||
AddWaitStep("wait a bit", 10);
|
||||
|
||||
AddUntilStep("wait for dialog display", () => Game.Dependencies.Get<DialogOverlay>().IsLoaded);
|
||||
|
||||
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == screenWithNestedStack);
|
||||
AddAssert("nested screen didn't change", () => screenWithNestedStack.SubScreenStack.CurrentScreen == screenWithNestedStack.Blocker);
|
||||
|
||||
AddAssert("did not perform", () => !actionPerformed);
|
||||
|
||||
AddAssert("only one exit attempt", () => screenWithNestedStack.Blocker.ExitAttempts == 1);
|
||||
|
||||
if (confirm)
|
||||
{
|
||||
AddStep("accept dialog", () => InputManager.Key(Key.Number1));
|
||||
AddAssert("nested screen changed", () => screenWithNestedStack.SubScreenStack.CurrentScreen != screenWithNestedStack.Blocker);
|
||||
AddUntilStep("did perform", () => actionPerformed);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddStep("cancel dialog", () => InputManager.Key(Key.Number2));
|
||||
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == screenWithNestedStack);
|
||||
AddAssert("nested screen didn't change", () => screenWithNestedStack.SubScreenStack.CurrentScreen == screenWithNestedStack.Blocker);
|
||||
AddAssert("did not perform", () => !actionPerformed);
|
||||
}
|
||||
}
|
||||
|
||||
private void importAndWaitForSongSelect()
|
||||
{
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||
@ -200,5 +240,30 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
return base.OnExiting(next);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestScreenWithNestedStack : OsuScreen, IHasSubScreenStack
|
||||
{
|
||||
public DialogBlockingScreen Blocker { get; private set; }
|
||||
|
||||
public ScreenStack SubScreenStack { get; } = new ScreenStack();
|
||||
|
||||
public TestScreenWithNestedStack()
|
||||
{
|
||||
AddInternal(SubScreenStack);
|
||||
|
||||
SubScreenStack.Push(Blocker = new DialogBlockingScreen());
|
||||
}
|
||||
|
||||
public override bool OnExiting(IScreen next)
|
||||
{
|
||||
if (SubScreenStack.CurrentScreen != null)
|
||||
{
|
||||
SubScreenStack.CurrentScreen.Exit();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnExiting(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,25 +4,58 @@
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestScenePopupDialog : OsuTestScene
|
||||
public class TestScenePopupDialog : OsuManualInputManagerTestScene
|
||||
{
|
||||
public TestScenePopupDialog()
|
||||
private TestPopupDialog dialog;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("new popup", () =>
|
||||
Add(new TestPopupDialog
|
||||
{
|
||||
Add(dialog = new TestPopupDialog
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Framework.Graphics.Containers.Visibility.Visible },
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDangerousButton([Values(false, true)] bool atEdge)
|
||||
{
|
||||
if (atEdge)
|
||||
{
|
||||
AddStep("move mouse to button edge", () =>
|
||||
{
|
||||
var dangerousButtonQuad = dialog.DangerousButton.ScreenSpaceDrawQuad;
|
||||
InputManager.MoveMouseTo(new Vector2(dangerousButtonQuad.TopLeft.X + 5, dangerousButtonQuad.Centre.Y));
|
||||
});
|
||||
}
|
||||
else
|
||||
AddStep("move mouse to button", () => InputManager.MoveMouseTo(dialog.DangerousButton));
|
||||
|
||||
AddStep("click button", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("action not invoked", () => !dialog.DangerousButtonInvoked);
|
||||
|
||||
AddStep("hold button", () => InputManager.PressButton(MouseButton.Left));
|
||||
AddUntilStep("action invoked", () => dialog.DangerousButtonInvoked);
|
||||
AddStep("release button", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
}
|
||||
|
||||
private class TestPopupDialog : PopupDialog
|
||||
{
|
||||
public PopupDialogDangerousButton DangerousButton { get; }
|
||||
|
||||
public bool DangerousButtonInvoked;
|
||||
|
||||
public TestPopupDialog()
|
||||
{
|
||||
Icon = FontAwesome.Solid.AssistiveListeningSystems;
|
||||
@ -40,9 +73,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
Text = @"You're a fake!",
|
||||
},
|
||||
new PopupDialogDangerousButton
|
||||
DangerousButton = new PopupDialogDangerousButton
|
||||
{
|
||||
Text = @"Careful with this one..",
|
||||
Action = () => DangerousButtonInvoked = true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
114
osu.Game.Tests/Visual/UserInterface/TestSceneScalingContainer.cs
Normal file
114
osu.Game.Tests/Visual/UserInterface/TestSceneScalingContainer.cs
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -36,11 +36,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
|
||||
bool progressionToRight = q2.TopLeft.X > q1.TopLeft.X;
|
||||
|
||||
if (!progressionToRight)
|
||||
{
|
||||
var temp = q2;
|
||||
q2 = q1;
|
||||
q1 = temp;
|
||||
}
|
||||
(q2, q1) = (q1, q2);
|
||||
|
||||
var c1 = getCenteredVector(q1.TopRight, q1.BottomRight) + new Vector2(padding, 0);
|
||||
var c2 = getCenteredVector(q2.TopLeft, q2.BottomLeft) - new Vector2(padding, 0);
|
||||
|
@ -26,6 +26,11 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
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>
|
||||
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
|
||||
/// </summary>
|
||||
@ -40,7 +45,8 @@ namespace osu.Game.Beatmaps
|
||||
[CanBeNull]
|
||||
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;
|
||||
|
||||
@ -157,6 +163,9 @@ namespace osu.Game.Beatmaps
|
||||
if (string.IsNullOrEmpty(Metadata?.AudioFile))
|
||||
return null;
|
||||
|
||||
if (Metadata.AudioFile == virtual_track_filename)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return resources.Tracks.Get(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
|
||||
@ -173,6 +182,9 @@ namespace osu.Game.Beatmaps
|
||||
if (string.IsNullOrEmpty(Metadata?.AudioFile))
|
||||
return null;
|
||||
|
||||
if (Metadata.AudioFile == virtual_track_filename)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
|
||||
|
@ -70,7 +70,10 @@ namespace osu.Game.Graphics.Containers
|
||||
confirming = false;
|
||||
Fired = false;
|
||||
|
||||
this.TransformBindableTo(Progress, 0, fadeout_delay, Easing.Out);
|
||||
this
|
||||
.TransformBindableTo(Progress, Progress.Value)
|
||||
.Delay(200)
|
||||
.TransformBindableTo(Progress, 0, fadeout_delay, Easing.InSine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ namespace osu.Game.Graphics.Containers
|
||||
/// </summary>
|
||||
public class ScalingContainer : Container
|
||||
{
|
||||
private const float duration = 500;
|
||||
|
||||
private Bindable<float> sizeX;
|
||||
private Bindable<float> sizeY;
|
||||
private Bindable<float> posX;
|
||||
@ -82,6 +84,8 @@ namespace osu.Game.Graphics.Containers
|
||||
private readonly bool applyUIScale;
|
||||
private Bindable<float> uiScale;
|
||||
|
||||
private float currentScale = 1;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
public ScalingDrawSizePreservingFillContainer(bool applyUIScale)
|
||||
@ -95,14 +99,16 @@ namespace osu.Game.Graphics.Containers
|
||||
if (applyUIScale)
|
||||
{
|
||||
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);
|
||||
this.ResizeTo(new Vector2(1 / args.NewValue), 500, Easing.Out);
|
||||
Scale = new Vector2(currentScale);
|
||||
Size = new Vector2(1 / currentScale);
|
||||
|
||||
base.Update();
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,8 +146,6 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
private void updateSize()
|
||||
{
|
||||
const float duration = 500;
|
||||
|
||||
if (targetMode == ScalingMode.Everything)
|
||||
{
|
||||
// the top level scaling container manages the background to be displayed while scaling.
|
||||
|
@ -3,11 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace osu.Game.IO.Legacy
|
||||
@ -26,15 +22,6 @@ namespace osu.Game.IO.Legacy
|
||||
|
||||
public int RemainingBytes => (int)(stream.Length - stream.Position);
|
||||
|
||||
/// <summary> Static method to take a SerializationInfo object (an input to an ISerializable constructor)
|
||||
/// and produce a SerializationReader from which serialized objects can be read </summary>.
|
||||
public static SerializationReader GetReader(SerializationInfo info)
|
||||
{
|
||||
byte[] byteArray = (byte[])info.GetValue("X", typeof(byte[]));
|
||||
MemoryStream ms = new MemoryStream(byteArray);
|
||||
return new SerializationReader(ms);
|
||||
}
|
||||
|
||||
/// <summary> Reads a string from the buffer. Overrides the base implementation so it can cope with nulls. </summary>
|
||||
public override string ReadString()
|
||||
{
|
||||
@ -186,98 +173,12 @@ namespace osu.Game.IO.Legacy
|
||||
return ReadCharArray();
|
||||
|
||||
case ObjType.otherType:
|
||||
return DynamicDeserializer.Deserialize(BaseStream);
|
||||
throw new IOException("Deserialization of arbitrary type is not supported.");
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class DynamicDeserializer
|
||||
{
|
||||
private static VersionConfigToNamespaceAssemblyObjectBinder versionBinder;
|
||||
private static BinaryFormatter formatter;
|
||||
|
||||
private static void initialize()
|
||||
{
|
||||
versionBinder = new VersionConfigToNamespaceAssemblyObjectBinder();
|
||||
formatter = new BinaryFormatter
|
||||
{
|
||||
// AssemblyFormat = FormatterAssemblyStyle.Simple,
|
||||
Binder = versionBinder
|
||||
};
|
||||
}
|
||||
|
||||
public static object Deserialize(Stream stream)
|
||||
{
|
||||
if (formatter == null)
|
||||
initialize();
|
||||
|
||||
Debug.Assert(formatter != null, "formatter != null");
|
||||
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
return formatter.Deserialize(stream);
|
||||
}
|
||||
|
||||
#region Nested type: VersionConfigToNamespaceAssemblyObjectBinder
|
||||
|
||||
public sealed class VersionConfigToNamespaceAssemblyObjectBinder : SerializationBinder
|
||||
{
|
||||
private readonly Dictionary<string, Type> cache = new Dictionary<string, Type>();
|
||||
|
||||
public override Type BindToType(string assemblyName, string typeName)
|
||||
{
|
||||
if (cache.TryGetValue(assemblyName + typeName, out var typeToDeserialize))
|
||||
return typeToDeserialize;
|
||||
|
||||
List<Type> tmpTypes = new List<Type>();
|
||||
Type genType = null;
|
||||
|
||||
if (typeName.Contains("System.Collections.Generic") && typeName.Contains("[["))
|
||||
{
|
||||
string[] splitTypes = typeName.Split('[');
|
||||
|
||||
foreach (string typ in splitTypes)
|
||||
{
|
||||
if (typ.Contains("Version"))
|
||||
{
|
||||
string asmTmp = typ.Substring(typ.IndexOf(',') + 1);
|
||||
string asmName = asmTmp.Remove(asmTmp.IndexOf(']')).Trim();
|
||||
string typName = typ.Remove(typ.IndexOf(','));
|
||||
tmpTypes.Add(BindToType(asmName, typName));
|
||||
}
|
||||
else if (typ.Contains("Generic"))
|
||||
{
|
||||
genType = BindToType(assemblyName, typ);
|
||||
}
|
||||
}
|
||||
|
||||
if (genType != null && tmpTypes.Count > 0)
|
||||
{
|
||||
return genType.MakeGenericType(tmpTypes.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
string toAssemblyName = assemblyName.Split(',')[0];
|
||||
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
|
||||
foreach (Assembly a in assemblies)
|
||||
{
|
||||
if (a.FullName.Split(',')[0] == toAssemblyName)
|
||||
{
|
||||
typeToDeserialize = a.GetType(typeName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cache.Add(assemblyName + typeName, typeToDeserialize);
|
||||
|
||||
return typeToDeserialize;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
public enum ObjType : byte
|
||||
|
@ -4,9 +4,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Formatters;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Text;
|
||||
|
||||
// ReSharper disable ConditionIsAlwaysTrueOrFalse (we're allowing nulls to be passed to the writer where the underlying class doesn't).
|
||||
@ -218,25 +215,11 @@ namespace osu.Game.IO.Legacy
|
||||
break;
|
||||
|
||||
default:
|
||||
Write((byte)ObjType.otherType);
|
||||
BinaryFormatter b = new BinaryFormatter
|
||||
{
|
||||
// AssemblyFormat = FormatterAssemblyStyle.Simple,
|
||||
TypeFormat = FormatterTypeStyle.TypesWhenNeeded
|
||||
};
|
||||
b.Serialize(BaseStream, obj);
|
||||
break;
|
||||
throw new IOException("Serialization of arbitrary type is not supported.");
|
||||
} // switch
|
||||
} // if obj==null
|
||||
} // WriteObject
|
||||
|
||||
/// <summary> Adds the SerializationWriter buffer to the SerializationInfo at the end of GetObjectData(). </summary>
|
||||
public void AddToInfo(SerializationInfo info)
|
||||
{
|
||||
byte[] b = ((MemoryStream)BaseStream).ToArray();
|
||||
info.AddValue("X", b, typeof(byte[]));
|
||||
}
|
||||
|
||||
public void WriteRawBytes(byte[] b)
|
||||
{
|
||||
base.Write(b);
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// <summary>
|
||||
/// Invoked when any change occurs to the multiplayer room.
|
||||
/// </summary>
|
||||
public event Action? RoomUpdated;
|
||||
public virtual event Action? RoomUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a new user joins the room.
|
||||
@ -67,7 +67,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// <summary>
|
||||
/// Invoked when the multiplayer server requests the current beatmap to be loaded into play.
|
||||
/// </summary>
|
||||
public event Action? LoadRequested;
|
||||
public virtual event Action? LoadRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the multiplayer server requests gameplay to be started.
|
||||
@ -88,7 +88,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// <summary>
|
||||
/// The joined <see cref="MultiplayerRoom"/>.
|
||||
/// </summary>
|
||||
public MultiplayerRoom? Room
|
||||
public virtual MultiplayerRoom? Room
|
||||
{
|
||||
get
|
||||
{
|
||||
@ -114,12 +114,12 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// <summary>
|
||||
/// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available.
|
||||
/// </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>
|
||||
/// Whether the <see cref="LocalUser"/> is the host in <see cref="Room"/>.
|
||||
/// </summary>
|
||||
public bool IsHost
|
||||
public virtual bool IsHost
|
||||
{
|
||||
get
|
||||
{
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Online.Rooms
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class OnlinePlayBeatmapAvailabilityTracker : CompositeDrawable
|
||||
public class OnlinePlayBeatmapAvailabilityTracker : CompositeDrawable
|
||||
{
|
||||
public readonly IBindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
|
||||
|
||||
@ -41,7 +41,7 @@ namespace osu.Game.Online.Rooms
|
||||
/// <summary>
|
||||
/// The availability state of the currently selected playlist item.
|
||||
/// </summary>
|
||||
public IBindable<BeatmapAvailability> Availability => availability;
|
||||
public virtual IBindable<BeatmapAvailability> Availability => availability;
|
||||
|
||||
private readonly Bindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>(BeatmapAvailability.NotDownloaded());
|
||||
|
||||
|
@ -47,7 +47,10 @@ namespace osu.Game.Online
|
||||
Downloader.DownloadBegan += downloadBegan;
|
||||
Downloader.DownloadFailed += downloadFailed;
|
||||
|
||||
realmSubscription = realm.RegisterForNotifications(r => r.All<ScoreInfo>().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending), (items, changes, ___) =>
|
||||
realmSubscription = realm.RegisterForNotifications(r => r.All<ScoreInfo>().Where(s =>
|
||||
((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID)
|
||||
|| (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash))
|
||||
&& !s.DeletePending), (items, changes, ___) =>
|
||||
{
|
||||
if (items.Any())
|
||||
Schedule(() => UpdateState(DownloadState.LocallyAvailable));
|
||||
|
@ -253,7 +253,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
|
||||
noScoresPlaceholder.Hide();
|
||||
|
||||
if (Beatmap.Value == null || Beatmap.Value.OnlineID <= 0 || (Beatmap.Value?.BeatmapSet as IBeatmapSetOnlineInfo)?.Status <= BeatmapOnlineStatus.Pending)
|
||||
if (Beatmap.Value == null || Beatmap.Value.OnlineID <= 0 || (Beatmap.Value.BeatmapSet as IBeatmapSetOnlineInfo)?.Status <= BeatmapOnlineStatus.Pending)
|
||||
{
|
||||
Scores = null;
|
||||
Hide();
|
||||
|
@ -88,9 +88,13 @@ namespace osu.Game.Overlays.Dialog
|
||||
if (actionInvoked) return;
|
||||
|
||||
actionInvoked = true;
|
||||
action?.Invoke();
|
||||
|
||||
// Hide the dialog before running the action.
|
||||
// This is important as the code which is performed may check for a dialog being present (ie. `OsuGame.PerformFromScreen`)
|
||||
// and we don't want it to see the already dismissed dialog.
|
||||
Hide();
|
||||
|
||||
action?.Invoke();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -12,37 +12,38 @@ namespace osu.Game.Overlays.Dialog
|
||||
{
|
||||
public class PopupDialogDangerousButton : PopupDialogButton
|
||||
{
|
||||
private Box progressBox;
|
||||
private DangerousConfirmContainer confirmContainer;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
ButtonColour = colours.Red3;
|
||||
|
||||
ColourContainer.Add(new ConfirmFillBox
|
||||
ColourContainer.Add(progressBox = new Box
|
||||
{
|
||||
Action = () => Action(),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
});
|
||||
|
||||
AddInternal(confirmContainer = new DangerousConfirmContainer
|
||||
{
|
||||
Action = () => Action(),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
});
|
||||
}
|
||||
|
||||
private class ConfirmFillBox : HoldToConfirmContainer
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
private Box box;
|
||||
base.LoadComplete();
|
||||
|
||||
confirmContainer.Progress.BindValueChanged(progress => progressBox.Width = (float)progress.NewValue, true);
|
||||
}
|
||||
|
||||
private class DangerousConfirmContainer : HoldToConfirmContainer
|
||||
{
|
||||
protected override double? HoldActivationDelay => 500;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Child = box = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
Progress.BindValueChanged(progress => box.Width = (float)progress.NewValue, true);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
BeginConfirm();
|
||||
|
@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
|
||||
{
|
||||
LabelText = UserInterfaceStrings.HoldToConfirmActivationTime,
|
||||
Current = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay),
|
||||
Keywords = new[] { @"delay" },
|
||||
KeyboardStep = 50
|
||||
},
|
||||
};
|
||||
|
@ -23,7 +23,9 @@ namespace osu.Game.Overlays.Settings
|
||||
|
||||
private IBindable<SettingsSection> selectedSection;
|
||||
|
||||
private OsuSpriteText header;
|
||||
private Box dim;
|
||||
|
||||
private const float inactive_alpha = 0.8f;
|
||||
|
||||
public abstract Drawable CreateIcon();
|
||||
public abstract LocalisableString Header { get; }
|
||||
@ -78,25 +80,40 @@ namespace osu.Game.Overlays.Settings
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Top = 28,
|
||||
Bottom = 40,
|
||||
},
|
||||
Padding = new MarginPadding { Top = border_size },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
header = new OsuSpriteText
|
||||
new Container
|
||||
{
|
||||
Font = OsuFont.TorusAlternate.With(size: header_size),
|
||||
Text = Header,
|
||||
Margin = new MarginPadding
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
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()
|
||||
{
|
||||
float contentFade = 1;
|
||||
float headerFade = 1;
|
||||
float dimFade = 0;
|
||||
|
||||
if (!isCurrentSection)
|
||||
{
|
||||
contentFade = 0.25f;
|
||||
headerFade = IsHovered ? 0.5f : 0.25f;
|
||||
dimFade = IsHovered ? 0.5f : inactive_alpha;
|
||||
}
|
||||
|
||||
header.FadeTo(headerFade, 500, Easing.OutQuint);
|
||||
FlowContent.FadeTo(contentFade, 500, Easing.OutQuint);
|
||||
dim.FadeTo(dimFade, 300, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -197,7 +197,7 @@ namespace osu.Game.Overlays
|
||||
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()
|
||||
{
|
||||
|
@ -97,11 +97,14 @@ namespace osu.Game
|
||||
// if this has a sub stack, recursively check the screens within it.
|
||||
if (current is IHasSubScreenStack currentSubScreen)
|
||||
{
|
||||
if (findValidTarget(currentSubScreen.SubScreenStack.CurrentScreen))
|
||||
var nestedCurrent = currentSubScreen.SubScreenStack.CurrentScreen;
|
||||
|
||||
if (nestedCurrent != null)
|
||||
{
|
||||
// should be correct in theory, but currently untested/unused in existing implementations.
|
||||
current.MakeCurrent();
|
||||
return true;
|
||||
// note that calling findValidTarget actually performs the final operation.
|
||||
if (findValidTarget(nestedCurrent))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,6 +128,18 @@ namespace osu.Game
|
||||
/// <returns>Whether a dialog blocked interaction.</returns>
|
||||
private bool checkForDialog(IScreen current)
|
||||
{
|
||||
// An exit process may traverse multiple levels.
|
||||
// When checking for dismissing dialogs, let's also consider sub screens.
|
||||
while (current is IHasSubScreenStack currentWithSubScreenStack)
|
||||
{
|
||||
var nestedCurrent = currentWithSubScreenStack.SubScreenStack.CurrentScreen;
|
||||
|
||||
if (nestedCurrent == null)
|
||||
break;
|
||||
|
||||
current = nestedCurrent;
|
||||
}
|
||||
|
||||
var currentDialog = dialogOverlay.CurrentDialog;
|
||||
|
||||
if (lastEncounteredDialog != null)
|
||||
|
@ -133,6 +133,11 @@ namespace osu.Game.Rulesets.UI
|
||||
p.NewResult += (_, r) => NewResult?.Invoke(r);
|
||||
p.RevertResult += (_, r) => RevertResult?.Invoke(r);
|
||||
}));
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
IsPaused.ValueChanged += paused =>
|
||||
{
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Screens.Edit.GameplayTest
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
|
@ -185,8 +185,7 @@ namespace osu.Game.Screens.Menu
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
sampleHover = audio.Samples.Get(@"Menu/button-hover");
|
||||
if (!string.IsNullOrEmpty(sampleName))
|
||||
sampleClick = audio.Samples.Get($@"Menu/{sampleName}");
|
||||
sampleClick = audio.Samples.Get(!string.IsNullOrEmpty(sampleName) ? $@"Menu/{sampleName}" : @"UI/button-select");
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
|
@ -283,9 +283,15 @@ namespace osu.Game.Screens.Menu
|
||||
this.Delay(early_activation).Schedule(() =>
|
||||
{
|
||||
if (beatIndex % timingPoint.TimeSignature.Numerator == 0)
|
||||
sampleDownbeat.Play();
|
||||
{
|
||||
sampleDownbeat?.Play();
|
||||
}
|
||||
else
|
||||
sampleBeat.Play();
|
||||
{
|
||||
var channel = sampleBeat.GetChannel();
|
||||
channel.Frequency.Value = 0.95 + RNG.NextDouble(0.1);
|
||||
channel.Play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -24,12 +24,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
public class MultiplayerTeamResultsScreen : MultiplayerResultsScreen
|
||||
{
|
||||
private readonly SortedDictionary<int, BindableInt> teamScores;
|
||||
private readonly SortedDictionary<int, BindableLong> teamScores;
|
||||
|
||||
private Container winnerBackground;
|
||||
private Drawable winnerText;
|
||||
|
||||
public MultiplayerTeamResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, SortedDictionary<int, BindableInt> teamScores)
|
||||
public MultiplayerTeamResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, SortedDictionary<int, BindableLong> teamScores)
|
||||
: base(score, roomId, playlistItem)
|
||||
{
|
||||
if (teamScores.Count != 2)
|
||||
|
@ -17,8 +17,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
Bindable<bool> WaitingOnFrames { get; }
|
||||
|
||||
/// <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>
|
||||
/// <remarks>
|
||||
/// Of note, this will be false if this clock is *ahead* of the master clock.
|
||||
/// </remarks>
|
||||
bool IsCatchingUp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
@ -55,12 +55,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
public SpectatorGameplayClockContainer([NotNull] IClock 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()
|
||||
{
|
||||
// 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.
|
||||
if (SourceClock.IsRunning)
|
||||
Start();
|
||||
|
@ -164,7 +164,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
base.LoadComplete();
|
||||
|
||||
masterClockContainer.Reset();
|
||||
masterClockContainer.Stop();
|
||||
|
||||
syncManager.ReadyToStart += onReadyToStart;
|
||||
syncManager.MasterState.BindValueChanged(onMasterStateChanged, true);
|
||||
@ -198,8 +197,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
.DefaultIfEmpty(0)
|
||||
.Min();
|
||||
|
||||
masterClockContainer.Seek(startTime);
|
||||
masterClockContainer.Start();
|
||||
masterClockContainer.StartTime = startTime;
|
||||
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.
|
||||
canStartMasterClock = true;
|
||||
|
@ -106,7 +106,7 @@ namespace osu.Game.Screens.Play
|
||||
new Sprite
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Texture = beatmap?.Background,
|
||||
Texture = beatmap.Background,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
FillMode = FillMode.Fill,
|
||||
@ -126,7 +126,7 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = beatmap?.BeatmapInfo?.DifficultyName,
|
||||
Text = beatmap.BeatmapInfo.DifficultyName,
|
||||
Font = OsuFont.GetFont(size: 26, italics: true),
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Screens.Play
|
||||
/// <summary>
|
||||
/// Whether gameplay is paused.
|
||||
/// </summary>
|
||||
public readonly BindableBool IsPaused = new BindableBool();
|
||||
public readonly BindableBool IsPaused = new BindableBool(true);
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
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>
|
||||
/// Creates a new <see cref="GameplayClockContainer"/>.
|
||||
/// </summary>
|
||||
@ -106,16 +115,17 @@ namespace osu.Game.Screens.Play
|
||||
/// <summary>
|
||||
/// Resets this <see cref="GameplayClockContainer"/> and the source to an initial state ready for gameplay.
|
||||
/// </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.
|
||||
AdjustableSource.Stop();
|
||||
|
||||
if (!IsPaused.Value)
|
||||
if (!IsPaused.Value || startClock)
|
||||
Start();
|
||||
|
||||
ensureSourceClockSet();
|
||||
Seek(StartTime ?? 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -19,8 +19,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
private const float bar_height = 18;
|
||||
private const float font_size = 50;
|
||||
|
||||
public BindableInt Team1Score = new BindableInt();
|
||||
public BindableInt Team2Score = new BindableInt();
|
||||
public BindableLong Team1Score = new BindableLong();
|
||||
public BindableLong Team2Score = new BindableLong();
|
||||
|
||||
protected MatchScoreCounter Score1Text;
|
||||
protected MatchScoreCounter Score2Text;
|
||||
@ -133,7 +133,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
var winningBar = Team1Score.Value > Team2Score.Value ? score1Bar : score2Bar;
|
||||
var losingBar = Team1Score.Value <= Team2Score.Value ? score1Bar : score2Bar;
|
||||
|
||||
int diff = Math.Max(Team1Score.Value, Team2Score.Value) - Math.Min(Team1Score.Value, Team2Score.Value);
|
||||
long diff = Math.Max(Team1Score.Value, Team2Score.Value) - Math.Min(Team1Score.Value, Team2Score.Value);
|
||||
|
||||
losingBar.ResizeWidthTo(0, 400, Easing.OutQuint);
|
||||
winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint);
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>();
|
||||
|
||||
public readonly SortedDictionary<int, BindableInt> TeamScores = new SortedDictionary<int, BindableInt>();
|
||||
public readonly SortedDictionary<int, BindableLong> TeamScores = new SortedDictionary<int, BindableLong>();
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
@ -75,21 +75,27 @@ namespace osu.Game.Screens.Play.HUD
|
||||
foreach (var user in playingUsers)
|
||||
{
|
||||
var trackedUser = CreateUserData(user, ruleset, scoreProcessor);
|
||||
|
||||
trackedUser.ScoringMode.BindTo(scoringMode);
|
||||
trackedUser.Score.BindValueChanged(_ => Scheduler.AddOnce(updateTotals));
|
||||
|
||||
UserScores[user.UserID] = trackedUser;
|
||||
|
||||
if (trackedUser.Team is int team && !TeamScores.ContainsKey(team))
|
||||
TeamScores.Add(team, new BindableInt());
|
||||
TeamScores.Add(team, new BindableLong());
|
||||
}
|
||||
|
||||
userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray()).ContinueWith(task => Schedule(() =>
|
||||
{
|
||||
var users = task.GetResultSafely();
|
||||
|
||||
foreach (var user in users)
|
||||
for (int i = 0; i < users.Length; i++)
|
||||
{
|
||||
if (user == null)
|
||||
continue;
|
||||
var user = users[i] ?? new APIUser
|
||||
{
|
||||
Id = playingUsers[i].UserID,
|
||||
Username = "Unknown user",
|
||||
};
|
||||
|
||||
var trackedUser = UserScores[user.Id];
|
||||
|
||||
@ -175,8 +181,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
|
||||
trackedData.UpdateScore();
|
||||
|
||||
updateTotals();
|
||||
});
|
||||
|
||||
private void updateTotals()
|
||||
|
@ -46,36 +46,36 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
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 double gameplayStartTime;
|
||||
private readonly bool startAtGameplayStart;
|
||||
private readonly double firstHitObjectTime;
|
||||
|
||||
private HardwareCorrectionOffsetClock userGlobalOffsetClock;
|
||||
private HardwareCorrectionOffsetClock userBeatmapOffsetClock;
|
||||
private HardwareCorrectionOffsetClock platformOffsetClock;
|
||||
private MasterGameplayClock masterGameplayClock;
|
||||
private Bindable<double> userAudioOffset;
|
||||
private double startOffset;
|
||||
|
||||
private IDisposable beatmapOffsetSubscription;
|
||||
|
||||
private readonly double skipTargetTime;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; }
|
||||
|
||||
[Resolved]
|
||||
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)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
this.gameplayStartTime = gameplayStartTime;
|
||||
this.startAtGameplayStart = startAtGameplayStart;
|
||||
|
||||
firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime;
|
||||
this.skipTargetTime = skipTargetTime;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -90,41 +90,67 @@ namespace osu.Game.Screens.Play
|
||||
settings => settings.Offset,
|
||||
val => userBeatmapOffsetClock.Offset = val);
|
||||
|
||||
// sane default provided by ruleset.
|
||||
startOffset = gameplayStartTime;
|
||||
// Reset may have been called externally before LoadComplete.
|
||||
// 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)
|
||||
{
|
||||
startOffset = Math.Min(0, startOffset);
|
||||
// If a custom start time was not specified, calculate the best value to use.
|
||||
StartTime ??= findEarliestStartTime();
|
||||
|
||||
// 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)
|
||||
startOffset = Math.Min(startOffset, firstStoryboardEvent.Value);
|
||||
Reset(startClock: isStarted);
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (beatmap.BeatmapInfo.AudioLeadIn > 0)
|
||||
startOffset = Math.Min(startOffset, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn);
|
||||
}
|
||||
private double findEarliestStartTime()
|
||||
{
|
||||
// here we are trying to find the time to start playback from the "zero" point.
|
||||
// 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)
|
||||
{
|
||||
// The source is stopped by a frequency fade first.
|
||||
if (isPaused.NewValue)
|
||||
if (IsLoaded)
|
||||
{
|
||||
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)
|
||||
AdjustableSource.Stop();
|
||||
});
|
||||
this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ =>
|
||||
{
|
||||
if (IsPaused.Value == isPaused.NewValue)
|
||||
AdjustableSource.Stop();
|
||||
});
|
||||
}
|
||||
else
|
||||
this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In);
|
||||
}
|
||||
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()
|
||||
@ -152,10 +178,10 @@ namespace osu.Game.Screens.Play
|
||||
/// </summary>
|
||||
public void Skip()
|
||||
{
|
||||
if (GameplayClock.CurrentTime > gameplayStartTime - MINIMUM_SKIP_TIME)
|
||||
if (GameplayClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME)
|
||||
return;
|
||||
|
||||
double skipTarget = gameplayStartTime - MINIMUM_SKIP_TIME;
|
||||
double skipTarget = skipTargetTime - MINIMUM_SKIP_TIME;
|
||||
|
||||
if (GameplayClock.CurrentTime < 0 && skipTarget > 6000)
|
||||
// double skip exception for storyboards with very long intros
|
||||
@ -164,12 +190,6 @@ namespace osu.Game.Screens.Play
|
||||
Seek(skipTarget);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
Seek(startOffset);
|
||||
}
|
||||
|
||||
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.
|
||||
@ -278,7 +298,6 @@ namespace osu.Game.Screens.Play
|
||||
private class MasterGameplayClock : GameplayClock
|
||||
{
|
||||
public readonly List<Bindable<double>> MutableNonGameplayAdjustments = new List<Bindable<double>>();
|
||||
|
||||
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => MutableNonGameplayAdjustments;
|
||||
|
||||
public MasterGameplayClock(FramedOffsetClock underlyingClock)
|
||||
|
@ -607,30 +607,25 @@ namespace osu.Game.Screens.Play
|
||||
private ScheduledDelegate frameStablePlaybackResetDelegate;
|
||||
|
||||
/// <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>
|
||||
/// <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>
|
||||
/// <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.
|
||||
// See https://github.com/ppy/osu/issues/17267 for the issue.
|
||||
// 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();
|
||||
if (frameStablePlaybackResetDelegate?.Cancelled == false && !frameStablePlaybackResetDelegate.Completed)
|
||||
frameStablePlaybackResetDelegate.RunTask();
|
||||
|
||||
bool wasFrameStable = DrawableRuleset.FrameStablePlayback;
|
||||
DrawableRuleset.FrameStablePlayback = false;
|
||||
bool wasFrameStable = DrawableRuleset.FrameStablePlayback;
|
||||
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.
|
||||
frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable);
|
||||
});
|
||||
// Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek.
|
||||
frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -987,7 +982,7 @@ namespace osu.Game.Screens.Play
|
||||
if (GameplayClockContainer.GameplayClock.IsRunning)
|
||||
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)
|
||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -351,8 +351,7 @@ namespace osu.Game.Stores
|
||||
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
if (existing.DeletePending)
|
||||
UndeleteForReuse(existing);
|
||||
UndeleteForReuse(existing);
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
@ -388,9 +387,7 @@ namespace osu.Game.Stores
|
||||
{
|
||||
LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
|
||||
|
||||
if (existing.DeletePending)
|
||||
UndeleteForReuse(existing);
|
||||
|
||||
UndeleteForReuse(existing);
|
||||
transaction.Commit();
|
||||
|
||||
return existing.ToLive(Realm);
|
||||
@ -536,6 +533,10 @@ namespace osu.Game.Stores
|
||||
/// <param name="existing">The existing model.</param>
|
||||
protected virtual void UndeleteForReuse(TModel existing)
|
||||
{
|
||||
if (!existing.DeletePending)
|
||||
return;
|
||||
|
||||
LogForModel(existing, $@"Existing {HumanisedModelName}'s deletion flag has been removed to allow for reuse.");
|
||||
existing.DeletePending = false;
|
||||
}
|
||||
|
||||
|
@ -7,19 +7,16 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.Countdown;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
@ -141,16 +138,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
switch (Room.State)
|
||||
{
|
||||
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;
|
||||
|
||||
case MultiplayerRoomState.WaitingForLoad:
|
||||
@ -317,16 +304,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
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)
|
||||
{
|
||||
Debug.Assert(Room != null);
|
||||
@ -334,14 +311,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
switch (request)
|
||||
{
|
||||
case StartMatchCountdownRequest matchCountdownRequest:
|
||||
startCountdown(new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration }, StartMatch);
|
||||
break;
|
||||
|
||||
case StopCountdownRequest _:
|
||||
stopCountdown();
|
||||
break;
|
||||
|
||||
case ChangeTeamRequest changeTeam:
|
||||
|
||||
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()
|
||||
{
|
||||
Debug.Assert(Room != null);
|
||||
|
@ -30,13 +30,13 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="ppy.LocalisationAnalyser" Version="2022.320.0">
|
||||
<PackageReference Include="ppy.LocalisationAnalyser" Version="2022.417.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="10.10.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.408.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.407.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.415.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.415.0" />
|
||||
<PackageReference Include="Sentry" Version="3.14.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.30.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
|
@ -61,8 +61,8 @@
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.408.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.407.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.415.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.415.0" />
|
||||
</ItemGroup>
|
||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
|
||||
<PropertyGroup>
|
||||
@ -84,7 +84,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
|
||||
<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="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
|
@ -118,6 +118,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MissingLinebreak/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MissingSpace/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MoreSpecificForeachVariableTypeAvailable/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MoveVariableDeclarationInsideLoopCondition/@EntryIndexedValue">HINT</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MultipleSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MultipleStatementsOnOneLine/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MultipleTypeMembersOnOneLine/@EntryIndexedValue">WARNING</s:String>
|
||||
|
Loading…
Reference in New Issue
Block a user