mirror of
https://github.com/SK-la/Ez2Lazer.git
synced 2026-03-13 11:20:28 +00:00
同步更新,增加可选EZ风判定图,优化EZ候选列表
This commit is contained in:
@@ -25,7 +25,3 @@ M:TagLib.File.Create(System.String);TagLib's MIME type detection changes behavio
|
|||||||
M:TagLib.File.Create(TagLib.File.IFileAbstraction);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
M:TagLib.File.Create(TagLib.File.IFileAbstraction);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||||
M:TagLib.File.Create(System.String,TagLib.ReadStyle);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
M:TagLib.File.Create(System.String,TagLib.ReadStyle);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||||
M:TagLib.File.Create(TagLib.File.IFileAbstraction,TagLib.ReadStyle);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
M:TagLib.File.Create(TagLib.File.IFileAbstraction,TagLib.ReadStyle);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||||
M:TagLib.File.Create(System.String);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
|
||||||
M:TagLib.File.Create(TagLib.File.IFileAbstraction);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
|
||||||
M:TagLib.File.Create(System.String,TagLib.ReadStyle);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
|
||||||
M:TagLib.File.Create(TagLib.File.IFileAbstraction,TagLib.ReadStyle);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.419.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.512.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
{
|
{
|
||||||
"solution": {
|
"solution": {
|
||||||
"path": "osu.sln",
|
"path": "osu.sln",
|
||||||
"projects": [
|
"projects": [
|
||||||
"..\\osu-resources\\osu.Game.Resources\\osu.Game.Resources.csproj",
|
"..\\osu-resources\\osu.Game.Resources\\osu.Game.Resources.csproj",
|
||||||
"osu.Desktop\\osu.Desktop.csproj",
|
"osu.Desktop\\osu.Desktop.csproj",
|
||||||
"osu.Game.Benchmarks\\osu.Game.Benchmarks.csproj",
|
"osu.Game.Benchmarks\\osu.Game.Benchmarks.csproj",
|
||||||
"osu.Game.Rulesets.Catch.Tests\\osu.Game.Rulesets.Catch.Tests.csproj",
|
"osu.Game.Rulesets.Catch.Tests\\osu.Game.Rulesets.Catch.Tests.csproj",
|
||||||
"osu.Game.Rulesets.Catch\\osu.Game.Rulesets.Catch.csproj",
|
"osu.Game.Rulesets.Catch\\osu.Game.Rulesets.Catch.csproj",
|
||||||
"osu.Game.Rulesets.Mania.Tests\\osu.Game.Rulesets.Mania.Tests.csproj",
|
"osu.Game.Rulesets.Mania.Tests\\osu.Game.Rulesets.Mania.Tests.csproj",
|
||||||
"osu.Game.Rulesets.Mania\\osu.Game.Rulesets.Mania.csproj",
|
"osu.Game.Rulesets.Mania\\osu.Game.Rulesets.Mania.csproj",
|
||||||
"osu.Game.Rulesets.Osu.Tests\\osu.Game.Rulesets.Osu.Tests.csproj",
|
"osu.Game.Rulesets.Osu.Tests\\osu.Game.Rulesets.Osu.Tests.csproj",
|
||||||
"osu.Game.Rulesets.Osu\\osu.Game.Rulesets.Osu.csproj",
|
"osu.Game.Rulesets.Osu\\osu.Game.Rulesets.Osu.csproj",
|
||||||
"osu.Game.Rulesets.Taiko.Tests\\osu.Game.Rulesets.Taiko.Tests.csproj",
|
"osu.Game.Rulesets.Taiko.Tests\\osu.Game.Rulesets.Taiko.Tests.csproj",
|
||||||
"osu.Game.Rulesets.Taiko\\osu.Game.Rulesets.Taiko.csproj",
|
"osu.Game.Rulesets.Taiko\\osu.Game.Rulesets.Taiko.csproj",
|
||||||
"osu.Game.Tests\\osu.Game.Tests.csproj",
|
"osu.Game.Tests\\osu.Game.Tests.csproj",
|
||||||
"osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj",
|
"osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj",
|
||||||
"osu.Game.Tournament\\osu.Game.Tournament.csproj",
|
"osu.Game.Tournament\\osu.Game.Tournament.csproj",
|
||||||
"osu.Game\\osu.Game.csproj",
|
"osu.Game\\osu.Game.csproj",
|
||||||
"Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform.Tests\\osu.Game.Rulesets.EmptyFreeform.Tests.csproj",
|
"Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform.Tests\\osu.Game.Rulesets.EmptyFreeform.Tests.csproj",
|
||||||
"Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj",
|
"Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj",
|
||||||
"Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
|
"Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
|
||||||
"Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
|
"Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
|
||||||
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj",
|
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj",
|
||||||
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj",
|
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj",
|
||||||
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
|
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
|
||||||
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
|
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj"
|
||||||
"../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj"
|
]
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
75
osu.Game.Rulesets.Catch.Tests/TestSceneReplayRecording.cs
Normal file
75
osu.Game.Rulesets.Catch.Tests/TestSceneReplayRecording.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Catch.Replays;
|
||||||
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
|
using osu.Game.Storyboards;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Tests
|
||||||
|
{
|
||||||
|
public partial class TestSceneReplayRecording : PlayerTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private AudioManager audioManager { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new Fruit { StartTime = 0, },
|
||||||
|
new Fruit { StartTime = 5000, },
|
||||||
|
new Fruit { StartTime = 10000, },
|
||||||
|
new Fruit { StartTime = 15000, }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
|
||||||
|
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRecording()
|
||||||
|
{
|
||||||
|
seekTo(0);
|
||||||
|
AddStep("start moving left", () => InputManager.PressKey(Key.Left));
|
||||||
|
seekTo(5000);
|
||||||
|
AddStep("end moving left", () => InputManager.ReleaseKey(Key.Left));
|
||||||
|
AddAssert("catcher max left", () => this.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(0));
|
||||||
|
AddAssert("movement to left recorded to replay", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => f.Actions.SequenceEqual([CatchAction.MoveLeft])));
|
||||||
|
AddAssert("replay reached left edge", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => Precision.AlmostEquals(f.Position, 0)));
|
||||||
|
|
||||||
|
AddStep("start dashing right", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.LShift);
|
||||||
|
InputManager.PressKey(Key.Right);
|
||||||
|
});
|
||||||
|
seekTo(10000);
|
||||||
|
AddStep("end dashing right", () =>
|
||||||
|
{
|
||||||
|
InputManager.ReleaseKey(Key.LShift);
|
||||||
|
InputManager.ReleaseKey(Key.Right);
|
||||||
|
});
|
||||||
|
AddAssert("catcher max right", () => this.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(CatchPlayfield.WIDTH));
|
||||||
|
AddAssert("dash to right recorded to replay", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => f.Actions.SequenceEqual([CatchAction.Dash, CatchAction.MoveRight])));
|
||||||
|
AddAssert("replay reached right edge", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => Precision.AlmostEquals(f.Position, CatchPlayfield.WIDTH)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seekTo(double time)
|
||||||
|
{
|
||||||
|
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
|
||||||
|
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
|
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
|
||||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||||
|
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||||
|
|
||||||
if (keyCounter != null)
|
if (keyCounter != null)
|
||||||
{
|
{
|
||||||
@@ -64,12 +65,20 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
|||||||
spectatorList.Origin = Anchor.BottomLeft;
|
spectatorList.Origin = Anchor.BottomLeft;
|
||||||
spectatorList.Position = new Vector2(10, -10);
|
spectatorList.Position = new Vector2(10, -10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (leaderboard != null)
|
||||||
|
{
|
||||||
|
leaderboard.Anchor = Anchor.CentreLeft;
|
||||||
|
leaderboard.Origin = Anchor.CentreLeft;
|
||||||
|
leaderboard.X = 10;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new LegacyKeyCounterDisplay(),
|
new LegacyKeyCounterDisplay(),
|
||||||
new SpectatorList(),
|
new SpectatorList(),
|
||||||
|
new DrawableGameplayLeaderboard(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,12 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
|||||||
public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss);
|
public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss);
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestGreatHit() => CreateModTest(new ModTestData
|
public void TestPerfectHits([Values] bool requirePerfectHits) => CreateModTest(new ModTestData
|
||||||
{
|
{
|
||||||
Mod = new ManiaModPerfect(),
|
Mod = new ManiaModPerfect
|
||||||
|
{
|
||||||
|
RequirePerfectHits = { Value = requirePerfectHits }
|
||||||
|
},
|
||||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
|
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
|
||||||
Autoplay = false,
|
Autoplay = false,
|
||||||
CreateBeatmap = () => new Beatmap
|
CreateBeatmap = () => new Beatmap
|
||||||
@@ -47,6 +50,32 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
ReplayFrames = new List<ReplayFrame>
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new ManiaReplayFrame(1000, ManiaAction.Key1),
|
||||||
|
new ManiaReplayFrame(2000)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGreatHit([Values] bool requirePerfectHits) => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new ManiaModPerfect
|
||||||
|
{
|
||||||
|
RequirePerfectHits = { Value = requirePerfectHits }
|
||||||
|
},
|
||||||
|
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(requirePerfectHits),
|
||||||
|
Autoplay = false,
|
||||||
|
CreateBeatmap = () => new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Note
|
||||||
|
{
|
||||||
|
StartTime = 1000,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
{
|
{
|
||||||
new ManiaReplayFrame(1020, ManiaAction.Key1),
|
new ManiaReplayFrame(1020, ManiaAction.Key1),
|
||||||
new ManiaReplayFrame(2000)
|
new ManiaReplayFrame(2000)
|
||||||
|
|||||||
59
osu.Game.Rulesets.Mania.Tests/TestSceneReplayRecording.cs
Normal file
59
osu.Game.Rulesets.Mania.Tests/TestSceneReplayRecording.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mania.Replays;
|
||||||
|
using osu.Game.Storyboards;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests
|
||||||
|
{
|
||||||
|
public partial class TestSceneReplayRecording : PlayerTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private AudioManager audioManager { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new ManiaBeatmap(new StageDefinition(1))
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new Note { StartTime = 0, },
|
||||||
|
new Note { StartTime = 5000, },
|
||||||
|
new Note { StartTime = 10000, },
|
||||||
|
new Note { StartTime = 15000, }
|
||||||
|
},
|
||||||
|
Difficulty = { CircleSize = 1 },
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
Ruleset = ruleset,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
|
||||||
|
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRecording()
|
||||||
|
{
|
||||||
|
seekTo(0);
|
||||||
|
AddStep("press space", () => InputManager.Key(Key.Space));
|
||||||
|
AddAssert("button press recorded to replay", () => Player.Score.Replay.Frames.OfType<ManiaReplayFrame>().Any(f => f.Actions.SequenceEqual([ManiaAction.Key1])));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seekTo(double time)
|
||||||
|
{
|
||||||
|
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
|
||||||
|
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
|||||||
SetDefault(ManiaRulesetSetting.ScrollStyle, ManiaScrollingStyle.ScrollTimeStyleFixed);
|
SetDefault(ManiaRulesetSetting.ScrollStyle, ManiaScrollingStyle.ScrollTimeStyleFixed);
|
||||||
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
|
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
|
||||||
SetDefault(ManiaRulesetSetting.ScrollPerKeyMode, false);
|
SetDefault(ManiaRulesetSetting.ScrollPerKeyMode, false);
|
||||||
|
SetDefault(ManiaRulesetSetting.PerspectiveAngle, 90.0f, 30.0f, 90.0f);
|
||||||
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, true);
|
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, true);
|
||||||
SetDefault(ManiaRulesetSetting.MobileLayout, ManiaMobileLayout.Portrait);
|
SetDefault(ManiaRulesetSetting.MobileLayout, ManiaMobileLayout.Portrait);
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
|||||||
ScrollStyle,
|
ScrollStyle,
|
||||||
// HitMode,
|
// HitMode,
|
||||||
|
|
||||||
|
PerspectiveAngle,
|
||||||
ColumnWidth,
|
ColumnWidth,
|
||||||
SpecialFactor,
|
SpecialFactor,
|
||||||
ScrollPerKeyMode,
|
ScrollPerKeyMode,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
namespace osu.Game.Rulesets.Mania.LAsEZMania
|
||||||
{
|
{
|
||||||
public partial class ManiaActionInputTrigger : InputTrigger
|
public partial class ManiaActionInputTrigger : InputTrigger
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@@ -9,13 +11,16 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
{
|
{
|
||||||
public class ManiaModPerfect : ModPerfect
|
public class ManiaModPerfect : ModPerfect
|
||||||
{
|
{
|
||||||
|
[SettingSource("Require perfect hits")]
|
||||||
|
public BindableBool RequirePerfectHits { get; } = new BindableBool();
|
||||||
|
|
||||||
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
|
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
|
||||||
{
|
{
|
||||||
if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type))
|
if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Mania allows imperfect "Great" hits without failing.
|
// Mania allows imperfect "Great" hits without failing.
|
||||||
if (result.Judgement.MaxResult == HitResult.Perfect)
|
if (result.Judgement.MaxResult == HitResult.Perfect && !RequirePerfectHits.Value)
|
||||||
return result.Type < HitResult.Great;
|
return result.Type < HitResult.Great;
|
||||||
|
|
||||||
return result.Type != result.Judgement.MaxResult;
|
return result.Type != result.Judgement.MaxResult;
|
||||||
|
|||||||
@@ -40,9 +40,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
|||||||
case GlobalSkinnableContainers.MainHUDComponents:
|
case GlobalSkinnableContainers.MainHUDComponents:
|
||||||
return new DefaultSkinComponentsContainer(container =>
|
return new DefaultSkinComponentsContainer(container =>
|
||||||
{
|
{
|
||||||
|
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||||
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
|
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
|
||||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (leaderboard != null)
|
||||||
|
leaderboard.Position = new Vector2(36, 115);
|
||||||
|
|
||||||
if (combo != null)
|
if (combo != null)
|
||||||
{
|
{
|
||||||
combo.ShowLabel.Value = false;
|
combo.ShowLabel.Value = false;
|
||||||
@@ -55,6 +59,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
|||||||
spectatorList.Position = new Vector2(36, -66);
|
spectatorList.Position = new Vector2(36, -66);
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
|
new DrawableGameplayLeaderboard(),
|
||||||
new ArgonManiaComboCounter(),
|
new ArgonManiaComboCounter(),
|
||||||
new SpectatorList
|
new SpectatorList
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,233 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Game.Configuration;
|
|
||||||
using osu.Game.Rulesets.Judgements;
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
|
||||||
using osu.Game.Rulesets.Scoring;
|
|
||||||
using osu.Game.Screens.Play.HUD;
|
|
||||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|
||||||
{
|
|
||||||
public partial class EzColumnHitErrorMeter : HitErrorMeter
|
|
||||||
{
|
|
||||||
[SettingSource("Icon Height", "Icon Height")]
|
|
||||||
public BindableNumber<float> IconHeight { get; } = new BindableNumber<float>(2)
|
|
||||||
{
|
|
||||||
MinValue = 1,
|
|
||||||
MaxValue = 20,
|
|
||||||
Precision = 1f,
|
|
||||||
};
|
|
||||||
|
|
||||||
[SettingSource("Move Height", "Move Height")]
|
|
||||||
public BindableNumber<float> MoveHeight { get; } = new BindableNumber<float>(10)
|
|
||||||
{
|
|
||||||
MinValue = 1,
|
|
||||||
MaxValue = 50,
|
|
||||||
Precision = 1f,
|
|
||||||
};
|
|
||||||
|
|
||||||
[SettingSource("Background Alpha", "Background Alpha")]
|
|
||||||
public BindableNumber<float> BackgroundAlpha { get; } = new BindableNumber<float>(0.2f)
|
|
||||||
{
|
|
||||||
MinValue = 0,
|
|
||||||
MaxValue = 1,
|
|
||||||
Precision = 0.1f,
|
|
||||||
};
|
|
||||||
|
|
||||||
private double[] floatingAverages = null!;
|
|
||||||
private Box[] judgementMarkers = null!;
|
|
||||||
private Container[] columns = null!;
|
|
||||||
private float keyCount;
|
|
||||||
|
|
||||||
private OsuConfigManager config = null!;
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private InputCountController controller { get; set; } = null!;
|
|
||||||
|
|
||||||
public EzColumnHitErrorMeter()
|
|
||||||
{
|
|
||||||
AutoSizeAxes = Axes.Both;
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(OsuConfigManager config)
|
|
||||||
{
|
|
||||||
this.config = config;
|
|
||||||
recreateComponents();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void recreateComponents()
|
|
||||||
{
|
|
||||||
keyCount = controller.Triggers.Count;
|
|
||||||
floatingAverages = new double[(int)keyCount];
|
|
||||||
judgementMarkers = new Box[(int)keyCount];
|
|
||||||
|
|
||||||
InternalChild = new Container
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Margin = new MarginPadding(2),
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
Spacing = new Vector2(0, 0),
|
|
||||||
Children = columns = Enumerable.Range(0, (int)keyCount).Select(index =>
|
|
||||||
{
|
|
||||||
var column = createColumn();
|
|
||||||
var marker = new Box
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
RelativePositionAxes = Axes.Y,
|
|
||||||
Blending = BlendingParameters.Additive,
|
|
||||||
Width = (float)config.Get<double>(OsuSetting.ColumnWidth),
|
|
||||||
Height = IconHeight.Value,
|
|
||||||
Colour = Colour4.Gray,
|
|
||||||
Alpha = 0.8f
|
|
||||||
};
|
|
||||||
column.Add(marker);
|
|
||||||
judgementMarkers[index] = marker;
|
|
||||||
return column;
|
|
||||||
}).ToArray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
controller.Triggers.BindCollectionChanged((_, __) => recreateComponents(), true);
|
|
||||||
|
|
||||||
// 更新标识块高度
|
|
||||||
IconHeight.BindValueChanged(height =>
|
|
||||||
{
|
|
||||||
foreach (var marker in judgementMarkers)
|
|
||||||
marker.Height = height.NewValue;
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
// 更新背景柱状列高度和标识块移动范围
|
|
||||||
MoveHeight.BindValueChanged(height =>
|
|
||||||
{
|
|
||||||
foreach (var column in columns)
|
|
||||||
{
|
|
||||||
var backgroundBox = column.Children.OfType<Box>().FirstOrDefault();
|
|
||||||
|
|
||||||
if (backgroundBox != null)
|
|
||||||
{
|
|
||||||
backgroundBox.Height = height.NewValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var marker in judgementMarkers)
|
|
||||||
{
|
|
||||||
marker.Y = Math.Clamp(marker.Y, -height.NewValue / 2, height.NewValue / 2);
|
|
||||||
// 重新计算标识块的相对位置
|
|
||||||
float currentAbsoluteY = marker.Y * height.OldValue;
|
|
||||||
float newRelativeY = currentAbsoluteY / height.NewValue;
|
|
||||||
|
|
||||||
marker.Y = newRelativeY;
|
|
||||||
|
|
||||||
// 更新标识块的移动范围
|
|
||||||
marker.MoveToY(newRelativeY, 800, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
|
|
||||||
Invalidate(Invalidation.DrawSize);
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
// 更新背景透明度
|
|
||||||
BackgroundAlpha.BindValueChanged(alpha =>
|
|
||||||
{
|
|
||||||
foreach (var column in columns)
|
|
||||||
{
|
|
||||||
var backgroundBox = column.Children.OfType<Box>().FirstOrDefault();
|
|
||||||
if (backgroundBox != null)
|
|
||||||
backgroundBox.Alpha = alpha.NewValue;
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Container createColumn()
|
|
||||||
{
|
|
||||||
var backgroundBox = new Box
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Height = MoveHeight.Value,
|
|
||||||
Width = (float)config.Get<double>(OsuSetting.ColumnWidth),
|
|
||||||
Colour = Colour4.Gray,
|
|
||||||
Alpha = 0.2f
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Container
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
backgroundBox
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnNewJudgement(JudgementResult judgement)
|
|
||||||
{
|
|
||||||
if (!judgement.IsHit || !judgement.Type.IsScorable())
|
|
||||||
return;
|
|
||||||
|
|
||||||
int columnIndex = getColumnIndex(judgement);
|
|
||||||
if (columnIndex < 0 || columnIndex >= keyCount)
|
|
||||||
return;
|
|
||||||
|
|
||||||
floatingAverages[columnIndex] = floatingAverages[columnIndex] * 0.9 + judgement.TimeOffset * 0.1;
|
|
||||||
|
|
||||||
const int marker_move_duration = 800;
|
|
||||||
var marker = judgementMarkers[columnIndex];
|
|
||||||
|
|
||||||
float targetY = getRelativeJudgementPosition(floatingAverages[columnIndex]);
|
|
||||||
|
|
||||||
marker.Y = targetY;
|
|
||||||
|
|
||||||
marker.MoveToY(targetY, marker_move_duration, Easing.OutQuint);
|
|
||||||
|
|
||||||
marker.Colour = GetColourForHitResult(judgement.Type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private float getRelativeJudgementPosition(double value)
|
|
||||||
{
|
|
||||||
float moveRange = MoveHeight.Value;
|
|
||||||
return (float)Math.Clamp((value / HitWindows.WindowFor(HitResult.Miss)) * moveRange, -moveRange, moveRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getColumnIndex(JudgementResult judgement)
|
|
||||||
{
|
|
||||||
if (judgement.HitObject is IHasColumn hasColumn)
|
|
||||||
return hasColumn.Column;
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Clear()
|
|
||||||
{
|
|
||||||
foreach (var column in columns)
|
|
||||||
{
|
|
||||||
column.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
{
|
{
|
||||||
public partial class EzComComboCounter : ComboCounter
|
public partial class EzComComboCounter : ComboCounter
|
||||||
{
|
{
|
||||||
[SettingSource("Font", "Font", SettingControlType = typeof(OffsetNumberNameSelector))]
|
[SettingSource("Font", "Font", SettingControlType = typeof(EzEnumListSelector))]
|
||||||
public Bindable<OffsetNumberName> NameDropdown { get; } = new Bindable<OffsetNumberName>(OffsetNumberName.EZ2DJ_4th);
|
public Bindable<OffsetNumberName> NameDropdown { get; } = new Bindable<OffsetNumberName>((OffsetNumberName)4);
|
||||||
|
|
||||||
[SettingSource("Effect Type", "Effect Type")]
|
[SettingSource("Effect Type", "Effect Type")]
|
||||||
public Bindable<EffectType> Effect { get; } = new Bindable<EffectType>(EffectType.Scale);
|
public Bindable<EffectType> Effect { get; } = new Bindable<EffectType>(EffectType.Scale);
|
||||||
@@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
|
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
|
||||||
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
|
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
|
||||||
|
|
||||||
public EzCounterText Text = null!;
|
public EzComboText Text = null!;
|
||||||
protected override double RollingDuration => 250;
|
protected override double RollingDuration => 250;
|
||||||
protected virtual bool DisplayXSymbol => true;
|
protected virtual bool DisplayXSymbol => true;
|
||||||
|
|
||||||
@@ -142,9 +142,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
|
|
||||||
protected override IHasText CreateText()
|
protected override IHasText CreateText()
|
||||||
{
|
{
|
||||||
Text = new EzCounterText(NameDropdown)
|
Text = new EzComboText(NameDropdown)
|
||||||
{
|
{
|
||||||
Scale = new Vector2(2.2f),
|
Scale = new Vector2(1.8f),
|
||||||
};
|
};
|
||||||
return Text;
|
return Text;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
{
|
{
|
||||||
public partial class EzComComboSprite : HitErrorMeter
|
public partial class EzComComboSprite : HitErrorMeter
|
||||||
{
|
{
|
||||||
[SettingSource("Combo Text Font", "Combo Text Font", SettingControlType = typeof(OffsetNumberNameSelector))]
|
[SettingSource("Combo Text Font", "Combo Text Font", SettingControlType = typeof(EzEnumListSelector))]
|
||||||
public Bindable<OffsetNumberName> NameDropdown { get; } = new Bindable<OffsetNumberName>(OffsetNumberName.EZ2DJ_4th);
|
public Bindable<OffsetNumberName> NameDropdown { get; } = new Bindable<OffsetNumberName>((OffsetNumberName)4);
|
||||||
|
|
||||||
[SettingSource("Effect Type", "Effect Type")]
|
[SettingSource("Effect Type", "Effect Type")]
|
||||||
public Bindable<EffectType> Effect { get; } = new Bindable<EffectType>(EffectType.Scale);
|
public Bindable<EffectType> Effect { get; } = new Bindable<EffectType>(EffectType.Scale);
|
||||||
@@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
|
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
|
||||||
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
|
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
|
||||||
|
|
||||||
public EzCounterText Text = null!;
|
public EzComboText Text = null!;
|
||||||
|
|
||||||
public Bindable<int> Current { get; } = new Bindable<int>();
|
public Bindable<int> Current { get; } = new Bindable<int>();
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
{
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
Text = new EzCounterText(NameDropdown)
|
Text = new EzComboText(NameDropdown)
|
||||||
{
|
{
|
||||||
Scale = new Vector2(0.8f),
|
Scale = new Vector2(0.8f),
|
||||||
Text = "c",
|
Text = "c",
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Play.HUD.JudgementCounter;
|
using osu.Game.Screens.Play.HUD.JudgementCounter;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osu.Game.Skinning.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@@ -32,14 +32,28 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
Precision = 1f,
|
Precision = 1f,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[SettingSource("HitResult Text Font", "HitResult Text Font", SettingControlType = typeof(EzEnumListSelector))]
|
||||||
|
public Bindable<OffsetNumberName> NameDropdown { get; } = new Bindable<OffsetNumberName>((OffsetNumberName)49);
|
||||||
|
|
||||||
|
// [SettingSource("Effect Type", "Effect Type")]
|
||||||
|
// public Bindable<EffectType> Effect { get; } = new Bindable<EffectType>(EffectType.Scale);
|
||||||
|
//
|
||||||
|
// [SettingSource("Effect Origin", "Effect Origin", SettingControlType = typeof(AnchorDropdown))]
|
||||||
|
// public Bindable<Anchor> EffectOrigin { get; } = new Bindable<Anchor>(Anchor.TopCentre)
|
||||||
|
// {
|
||||||
|
// Default = Anchor.TopCentre,
|
||||||
|
// Value = Anchor.TopCentre
|
||||||
|
// };
|
||||||
|
|
||||||
private Vector2 dragStartPosition;
|
private Vector2 dragStartPosition;
|
||||||
private bool isDragging;
|
private bool isDragging;
|
||||||
|
public Sprite? StaticSprite;
|
||||||
|
private Container? fullComboSprite;
|
||||||
|
private Sample? fullComboSound;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private TextureStore textures { get; set; } = null!;
|
private TextureStore textures { get; set; } = null!;
|
||||||
|
|
||||||
protected HitWindows HitWindows { get; private set; } = null!;
|
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private ScoreProcessor processor { get; set; } = null!;
|
private ScoreProcessor processor { get; set; } = null!;
|
||||||
|
|
||||||
@@ -59,19 +73,27 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
}
|
}
|
||||||
|
|
||||||
// private (HitResult result, double length)[] hitWindows = null!;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(DrawableRuleset drawableRuleset)
|
private void load()
|
||||||
{
|
{
|
||||||
HitWindows = drawableRuleset.FirstAvailableHitWindows ?? HitWindows.Empty;
|
|
||||||
// hitWindows = HitWindows.GetAllAvailableWindows().ToArray();
|
|
||||||
// This is to allow the visual state to be correct after HUD comes visible after being hidden.
|
|
||||||
AlwaysPresent = true;
|
AlwaysPresent = true;
|
||||||
}
|
|
||||||
|
|
||||||
private Sprite? fullComboSprite;
|
fullComboSprite = new Container
|
||||||
private Sample? fullComboSound;
|
{
|
||||||
|
Child = new Sprite
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Scale = new Vector2(1.5f),
|
||||||
|
Alpha = 0, // 初始隐藏
|
||||||
|
Texture = textures.Get("EzResources/AllCombo/ALL-COMBO2"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AddInternal(fullComboSprite);
|
||||||
|
|
||||||
|
fullComboSound = sampleStore.Get("EzResources/AllCombo/full_combo_sound");
|
||||||
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
@@ -81,19 +103,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
|
|
||||||
processor.NewJudgement += processorNewJudgement;
|
processor.NewJudgement += processorNewJudgement;
|
||||||
|
|
||||||
// 加载 FULL COMBO 贴图和音效
|
NameDropdown.BindValueChanged(_ =>
|
||||||
fullComboSprite = new Sprite
|
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
ClearInternal(true);
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Scale = new Vector2(1.5f),
|
|
||||||
// Y = -20,
|
|
||||||
Alpha = 0, // 初始隐藏
|
|
||||||
Texture = textures.Get("Gameplay/AllCombo/ALL-COMBO2"), // 替换为你的贴图路径
|
|
||||||
};
|
|
||||||
AddInternal(fullComboSprite);
|
|
||||||
|
|
||||||
fullComboSound = sampleStore.Get("Gameplay/full_combo_sound"); // 替换为你的音效路径
|
StaticSprite?.Invalidate();
|
||||||
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processorNewJudgement(JudgementResult j)
|
private void processorNewJudgement(JudgementResult j)
|
||||||
@@ -115,12 +130,116 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
if (!judgement.Type.IsScorable() || judgement.Type.IsBonus())
|
if (!judgement.Type.IsScorable() || judgement.Type.IsBonus())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ClearInternal(true);
|
// 清除内部元素前先结束所有变换
|
||||||
|
StaticSprite?.FinishTransforms();
|
||||||
|
|
||||||
|
ClearInternal();
|
||||||
|
StaticSprite = null;
|
||||||
|
|
||||||
var judgementText = CreateJudgementTexture(judgement.Type);
|
var judgementText = CreateJudgementTexture(judgement.Type);
|
||||||
AddInternal(judgementText);
|
AddInternal(judgementText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Drawable CreateJudgementTexture(HitResult result)
|
||||||
|
{
|
||||||
|
string basePath = getGifPath(result);
|
||||||
|
var singleTexture = textures.Get($"{basePath}.png");
|
||||||
|
|
||||||
|
if (singleTexture != null)
|
||||||
|
{
|
||||||
|
StaticSprite = new Sprite
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Scale = new Vector2(0.5f),
|
||||||
|
Texture = singleTexture,
|
||||||
|
Alpha = 0,
|
||||||
|
// 设置混合模式
|
||||||
|
Blending = new BlendingParameters
|
||||||
|
{
|
||||||
|
// 1. 标准透明混合(最常用)
|
||||||
|
// Source = BlendingType.SrcAlpha,
|
||||||
|
// Destination = BlendingType.OneMinusSrcAlpha,
|
||||||
|
|
||||||
|
// 2. 加法混合(发光效果)
|
||||||
|
Source = BlendingType.SrcAlpha,
|
||||||
|
Destination = BlendingType.One,
|
||||||
|
|
||||||
|
// 3. 减法混合(暗色透明)
|
||||||
|
// Source = BlendingType.Zero,
|
||||||
|
// Destination = BlendingType.OneMinusSrcColor,
|
||||||
|
|
||||||
|
// 4. 纯色叠加(忽略黑色)
|
||||||
|
// Source = BlendingType.One,
|
||||||
|
// Destination = BlendingType.One,
|
||||||
|
|
||||||
|
// 5. 柔和混合
|
||||||
|
// Source = BlendingType.DstColor,
|
||||||
|
// Destination = BlendingType.One,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
PlayAnimation(result, StaticSprite);
|
||||||
|
});
|
||||||
|
|
||||||
|
return StaticSprite;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不存在单张图片时,尝试加载动画帧
|
||||||
|
var animation = new TextureAnimation
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Scale = new Vector2(1.2f),
|
||||||
|
DefaultFrameLength = 1000 / PlaybackFps.Value,
|
||||||
|
Loop = false
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0;; i++)
|
||||||
|
{
|
||||||
|
var texture = textures.Get($@"{basePath}/frame_{i}");
|
||||||
|
if (texture == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
animation.AddFrame(texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaybackFps.BindValueChanged(fps =>
|
||||||
|
{
|
||||||
|
animation.DefaultFrameLength = 1000 / fps.NewValue;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
PlayAnimationGif(result, animation);
|
||||||
|
|
||||||
|
animation.OnUpdate += _ =>
|
||||||
|
{
|
||||||
|
if (animation.CurrentFrameIndex == animation.FrameCount - 1)
|
||||||
|
animation.Expire();
|
||||||
|
};
|
||||||
|
|
||||||
|
return animation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string getGifPath(HitResult hitResult)
|
||||||
|
{
|
||||||
|
string textureNameReplace = NameDropdown.Value.ToString();
|
||||||
|
string basePath = $@"EzResources/GameTheme/{textureNameReplace}/judgement";
|
||||||
|
string resultName = hitResult switch
|
||||||
|
{
|
||||||
|
HitResult.Miss => "Miss",
|
||||||
|
HitResult.Meh => "Fail",
|
||||||
|
HitResult.Ok => "Fail",
|
||||||
|
HitResult.Good => "Good",
|
||||||
|
HitResult.Great => "Cool",
|
||||||
|
HitResult.Perfect => "Kool",
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
return $"{basePath}/{resultName}";
|
||||||
|
}
|
||||||
|
|
||||||
private void checkFullCombo()
|
private void checkFullCombo()
|
||||||
{
|
{
|
||||||
var missCounter = judgementCountController.Counters
|
var missCounter = judgementCountController.Counters
|
||||||
@@ -129,13 +248,132 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
if (missCounter.ResultCount.Value == 0 && fullComboSprite != null)
|
if (missCounter.ResultCount.Value == 0 && fullComboSprite != null)
|
||||||
{
|
{
|
||||||
// 显示 FULL COMBO 贴图
|
// 显示 FULL COMBO 贴图
|
||||||
fullComboSprite.FadeIn(200).Then().FadeOut(5000);
|
fullComboSprite.Alpha = 1;
|
||||||
|
fullComboSprite.FadeIn(50).Then().FadeOut(5000);
|
||||||
|
|
||||||
// 播放音效
|
// 播放音效
|
||||||
fullComboSound?.Play();
|
fullComboSound?.Play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual void PlayAnimationGif(HitResult hitResult, Drawable drawable)
|
||||||
|
{
|
||||||
|
const float flash_speed = 60f;
|
||||||
|
applyFadeEffect(hitResult, drawable, flash_speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFadeEffect(HitResult hitResult, Drawable drawable, double flashSpeed)
|
||||||
|
{
|
||||||
|
if (!drawable.IsLoaded)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 为每种判定结果定义颜色数组
|
||||||
|
var colors = hitResult switch
|
||||||
|
{
|
||||||
|
HitResult.Miss => new[] { Color4.Red, Color4.IndianRed },
|
||||||
|
HitResult.Meh => new[] { Color4.Purple, Color4.MediumPurple },
|
||||||
|
HitResult.Ok => new[] { Color4.ForestGreen, Color4.SeaGreen },
|
||||||
|
HitResult.Good => new[] { Color4.Green, Color4.LightGreen },
|
||||||
|
HitResult.Great => new[] { Color4.AliceBlue, Color4.LightSkyBlue },
|
||||||
|
HitResult.Perfect => new[] { Color4.LightBlue, Color4.LightGreen },
|
||||||
|
_ => new[] { Color4.White }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (drawable is TextureAnimation)
|
||||||
|
{
|
||||||
|
drawable.FadeColour(colors[0], 0);
|
||||||
|
var sequence = drawable.FadeColour(colors[0], flashSpeed, Easing.OutQuint);
|
||||||
|
|
||||||
|
for (int i = 1; i < colors.Length; i++)
|
||||||
|
{
|
||||||
|
sequence = sequence.Then().FadeColour(colors[i], flashSpeed, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// // 保持原有透明度,只将颜色调整为 20% 强度
|
||||||
|
// var fadedColors = colors.Select(c => new Color4(
|
||||||
|
// c.R * 0.5f + 0.5f, // 将颜色与白色混合,保持 20% 的原色
|
||||||
|
// c.G * 0.5f + 0.5f,
|
||||||
|
// c.B * 0.5f + 0.5f,
|
||||||
|
// 1f)).ToArray();
|
||||||
|
float[] weakerAlphas = new[] { 1f, 0.8f };
|
||||||
|
|
||||||
|
drawable.FadeTo(weakerAlphas[0], 0);
|
||||||
|
var sequence = drawable.FadeTo(weakerAlphas[0], flashSpeed, Easing.OutQuint);
|
||||||
|
|
||||||
|
for (int i = 1; i < weakerAlphas.Length; i++)
|
||||||
|
{
|
||||||
|
sequence = sequence.Then().FadeTo(weakerAlphas[i], flashSpeed, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void PlayAnimation(HitResult hitResult, Drawable drawable)
|
||||||
|
{
|
||||||
|
double flashSpeed = PlaybackFps.Value * 2;
|
||||||
|
applyFadeEffect(hitResult, drawable, flashSpeed);
|
||||||
|
|
||||||
|
switch (hitResult)
|
||||||
|
{
|
||||||
|
case HitResult.Perfect:
|
||||||
|
// 中心直接绘制最大状态,向上移动并拉长压扁消失
|
||||||
|
applyEzStyleEffect(drawable, new Vector2(1.2f), 15);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitResult.Great:
|
||||||
|
// 中心绘制,稍微放大后拉长压扁消失
|
||||||
|
applyEzStyleEffect(drawable, new Vector2(1.1f));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitResult.Good:
|
||||||
|
// 中心小状态,向上放大并移动后拉长压扁消失
|
||||||
|
applyEzStyleEffect(drawable, new Vector2(1f));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitResult.Ok:
|
||||||
|
case HitResult.Meh:
|
||||||
|
// 中心小状态,放大并向下移动后拉长压扁消失
|
||||||
|
applyEzStyleEffect(drawable, new Vector2(1f));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitResult.Miss:
|
||||||
|
// 中心小状态,放大后快速消失
|
||||||
|
applyEzStyleEffect(drawable, new Vector2(1f));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
applyEzStyleEffect(drawable, new Vector2(1.2f));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyEzStyleEffect(Drawable drawable, Vector2 scaleUp, float moveDistance = 0)
|
||||||
|
{
|
||||||
|
// 先结束之前的所有变换
|
||||||
|
drawable.FinishTransforms();
|
||||||
|
// 固定拉长压扁比例
|
||||||
|
var finalScale = new Vector2(1.5f, 0.05f);
|
||||||
|
// 固定动画持续时间
|
||||||
|
const double scale_up_duration = 150; // 放大动画
|
||||||
|
const double scale_down_duration = 180; // 压扁动画
|
||||||
|
const double fade_out_duration = scale_down_duration + 10; // 淡出动画
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
drawable.Alpha = 1;
|
||||||
|
// drawable.Position = Vector2.Zero;
|
||||||
|
|
||||||
|
drawable
|
||||||
|
// 第一步:放大动画,同时执行位移(如果有)
|
||||||
|
.ScaleTo(scaleUp, scale_up_duration, Easing.OutQuint)
|
||||||
|
.MoveTo(new Vector2(0, moveDistance), scale_up_duration, Easing.OutQuint)
|
||||||
|
.Delay(scale_up_duration + 20)
|
||||||
|
// 第二步:在放大基础上进行横向拉长和纵向压缩(使用固定比例)
|
||||||
|
.TransformTo(nameof(Scale), new Vector2(scaleUp.X * finalScale.X, scaleUp.Y * finalScale.Y), scale_down_duration, Easing.InQuint)
|
||||||
|
.MoveTo(new Vector2(0, -moveDistance / 10), fade_out_duration, Easing.InQuint)
|
||||||
|
.FadeOut(fade_out_duration, Easing.InQuint);
|
||||||
|
}
|
||||||
|
|
||||||
protected virtual void Clear()
|
protected virtual void Clear()
|
||||||
{
|
{
|
||||||
FinishTransforms(true);
|
FinishTransforms(true);
|
||||||
@@ -151,43 +389,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected TextureAnimation CreateJudgementTexture(HitResult result)
|
|
||||||
{
|
|
||||||
var judgementTexture = new TextureAnimation
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Scale = new Vector2(1.2f),
|
|
||||||
DefaultFrameLength = 1000 / PlaybackFps.Value,
|
|
||||||
Loop = false
|
|
||||||
};
|
|
||||||
|
|
||||||
string gifPath = getGifPath(result);
|
|
||||||
|
|
||||||
for (int i = 0;; i++)
|
|
||||||
{
|
|
||||||
var texture = textures.Get($@"{gifPath}/frame_{i}");
|
|
||||||
if (texture == null)
|
|
||||||
break;
|
|
||||||
|
|
||||||
judgementTexture.AddFrame(texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
PlaybackFps.BindValueChanged(fps =>
|
|
||||||
{
|
|
||||||
judgementTexture.DefaultFrameLength = 1000 / fps.NewValue;
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
PlayAnimation(result, judgementTexture);
|
|
||||||
|
|
||||||
judgementTexture.OnUpdate += _ =>
|
|
||||||
{
|
|
||||||
if (judgementTexture.CurrentFrameIndex == judgementTexture.FrameCount - 1)
|
|
||||||
judgementTexture.Expire();
|
|
||||||
};
|
|
||||||
return judgementTexture;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnDragStart(DragStartEvent e)
|
protected override bool OnDragStart(DragStartEvent e)
|
||||||
{
|
{
|
||||||
dragStartPosition = e.ScreenSpaceMousePosition;
|
dragStartPosition = e.ScreenSpaceMousePosition;
|
||||||
@@ -209,70 +410,5 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
{
|
{
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string getGifPath(HitResult hitResult)
|
|
||||||
{
|
|
||||||
return hitResult switch
|
|
||||||
{
|
|
||||||
HitResult.Miss => @"Gameplay/Ez2/score/Miss",
|
|
||||||
HitResult.Meh => @"Gameplay/Ez2/score/Fail1",
|
|
||||||
HitResult.Ok => @"Gameplay/Ez2/score/Fail",
|
|
||||||
HitResult.Good => @"Gameplay/Ez2/score/Good",
|
|
||||||
HitResult.Great => @"Gameplay/Ez2/score/Cool",
|
|
||||||
HitResult.Perfect => @"Gameplay/Ez2/score/Kool",
|
|
||||||
_ => @"Gameplay/Ez2/score",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void PlayAnimation(HitResult hitResult, Drawable drawable)
|
|
||||||
{
|
|
||||||
const float flash_speed = 60f;
|
|
||||||
|
|
||||||
switch (hitResult)
|
|
||||||
{
|
|
||||||
case HitResult.Miss:
|
|
||||||
applyFadeEffect(drawable, new[] { Color4.Red, Color4.IndianRed }, flash_speed);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HitResult.Meh:
|
|
||||||
applyFadeEffect(drawable, new[] { Color4.Purple, Color4.MediumPurple }, flash_speed);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HitResult.Ok:
|
|
||||||
applyFadeEffect(drawable, new[] { Color4.ForestGreen, Color4.SeaGreen }, flash_speed);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HitResult.Good:
|
|
||||||
applyFadeEffect(drawable, new[] { Color4.Green, Color4.LightGreen }, flash_speed);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HitResult.Great:
|
|
||||||
applyFadeEffect(drawable, new[] { Color4.AliceBlue, Color4.LightSkyBlue }, flash_speed);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HitResult.Perfect:
|
|
||||||
applyFadeEffect(drawable, new[] { Color4.LightBlue, Color4.LightGreen }, flash_speed);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyFadeEffect(Drawable drawable, Color4[] colors, double flashSpeed)
|
|
||||||
{
|
|
||||||
if (!drawable.IsLoaded)
|
|
||||||
return;
|
|
||||||
|
|
||||||
drawable.FadeColour(colors[0], 0);
|
|
||||||
var sequence = drawable.FadeColour(colors[0], flashSpeed, Easing.OutQuint);
|
|
||||||
|
|
||||||
for (int i = 1; i < colors.Length; i++)
|
|
||||||
{
|
|
||||||
sequence = sequence.Then().FadeColour(colors[i], flashSpeed, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
|
|
||||||
// sequence.Loop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
[Cached]
|
[Cached]
|
||||||
public partial class EzComHitTiming : HitErrorMeter
|
public partial class EzComHitTiming : HitErrorMeter
|
||||||
{
|
{
|
||||||
[SettingSource("Offset Number Font", "Offset Number Font", SettingControlType = typeof(OffsetNumberNameSelector))]
|
[SettingSource("Offset Number Font", "Offset Number Font", SettingControlType = typeof(EzEnumListSelector))]
|
||||||
public Bindable<OffsetNumberName> NumberNameDropdown { get; } = new Bindable<OffsetNumberName>(OffsetNumberName.Tomato);
|
public Bindable<OffsetNumberName> NumberNameDropdown { get; } = new Bindable<OffsetNumberName>((OffsetNumberName)28);
|
||||||
|
|
||||||
[SettingSource("Offset Text Font", "Offset Text Font", SettingControlType = typeof(OffsetTextNameSelector))]
|
[SettingSource("Offset Text Font", "Offset Text Font", SettingControlType = typeof(OffsetTextNameSelector))]
|
||||||
public Bindable<OffsetNumberName> TextNameDropdown { get; } = new Bindable<OffsetNumberName>(OffsetNumberName.Tomato);
|
public Bindable<OffsetNumberName> TextNameDropdown { get; } = new Bindable<OffsetNumberName>((OffsetNumberName)28);
|
||||||
|
|
||||||
[SettingSource("AloneShow", "Show only Early or: Late separately")]
|
[SettingSource("AloneShow", "Show only Early or: Late separately")]
|
||||||
public Bindable<AloneShowMenu> AloneShow { get; } = new Bindable<AloneShowMenu>(AloneShowMenu.None);
|
public Bindable<AloneShowMenu> AloneShow { get; } = new Bindable<AloneShowMenu>(AloneShowMenu.None);
|
||||||
@@ -63,10 +63,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
|
|
||||||
private Container timingContainer = null!;
|
private Container timingContainer = null!;
|
||||||
private FillFlowContainer errorContainer = null!;
|
private FillFlowContainer errorContainer = null!;
|
||||||
private EzCounterText timingTextL = null!;
|
private EzComboText timingTextL = null!;
|
||||||
private EzCounterText timingText = null!;
|
private EzComboText timingText = null!;
|
||||||
private EzCounterText timingTextR = null!;
|
private EzComboText timingTextR = null!;
|
||||||
private EzCounterText offsetText = null!;
|
private EzComboText offsetText = null!;
|
||||||
private Box backgroundBox = null!;
|
private Box backgroundBox = null!;
|
||||||
|
|
||||||
public EzComHitTiming()
|
public EzComHitTiming()
|
||||||
@@ -101,11 +101,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
AutoSizeAxes = Axes.Both,
|
AutoSizeAxes = Axes.Both,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Scale = new Vector2(0.8f),
|
Scale = new Vector2(2f),
|
||||||
// Spacing = new Vector2(SymmetryOffset.Value),
|
// Spacing = new Vector2(SymmetryOffset.Value),
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
timingTextL = new EzCounterText(TextNameDropdown)
|
timingTextL = new EzComboText(TextNameDropdown)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@@ -113,14 +113,14 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
Alpha = 1,
|
Alpha = 1,
|
||||||
Position = new Vector2(-SymmetryOffset.Value, 0)
|
Position = new Vector2(-SymmetryOffset.Value, 0)
|
||||||
},
|
},
|
||||||
timingText = new EzCounterText(TextNameDropdown)
|
timingText = new EzComboText(TextNameDropdown)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Text = "e/l",
|
Text = "e/l",
|
||||||
Alpha = 0
|
Alpha = 0
|
||||||
},
|
},
|
||||||
timingTextR = new EzCounterText(TextNameDropdown)
|
timingTextR = new EzComboText(TextNameDropdown)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@@ -130,11 +130,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
offsetText = new EzCounterText(NumberNameDropdown)
|
offsetText = new EzComboText(NumberNameDropdown)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Scale = new Vector2(1.2f),
|
Scale = new Vector2(1.5f),
|
||||||
Text = "±000",
|
Text = "±000",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
|||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class OffsetTextNameSelector : OffsetNumberNameSelector
|
public partial class OffsetTextNameSelector : EzEnumListSelector
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ using osu.Framework.Utils;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD;
|
using osu.Game.Rulesets.Mania.LAsEZMania;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
|||||||
@@ -128,13 +128,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
|||||||
keyCounter.Position = new Vector2(0, -Stage.HIT_TARGET_POSITION - stage_padding_bottom);
|
keyCounter.Position = new Vector2(0, -Stage.HIT_TARGET_POSITION - stage_padding_bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
var columnHitErrorMeter = container.OfType<EzColumnHitErrorMeter>().FirstOrDefault();
|
var columnHitErrorMeter = container.OfType<EzComHitTimingColumns>().FirstOrDefault();
|
||||||
|
|
||||||
if (columnHitErrorMeter != null)
|
if (columnHitErrorMeter != null)
|
||||||
{
|
{
|
||||||
columnHitErrorMeter.Anchor = Anchor.Centre;
|
columnHitErrorMeter.Anchor = Anchor.Centre;
|
||||||
columnHitErrorMeter.Origin = Anchor.Centre;
|
columnHitErrorMeter.Origin = Anchor.Centre;
|
||||||
columnHitErrorMeter.Position = new Vector2(0, 20);
|
columnHitErrorMeter.Y = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hitErrorMeter = container.OfType<BarHitErrorMeter>().FirstOrDefault();
|
var hitErrorMeter = container.OfType<BarHitErrorMeter>().FirstOrDefault();
|
||||||
@@ -167,7 +167,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
|||||||
new EzComComboCounter(),
|
new EzComComboCounter(),
|
||||||
new EzComComboCounter(),
|
new EzComComboCounter(),
|
||||||
new EzComKeyCounterDisplay(),
|
new EzComKeyCounterDisplay(),
|
||||||
new EzColumnHitErrorMeter(),
|
new EzComHitTimingColumns(),
|
||||||
new BarHitErrorMeter(),
|
new BarHitErrorMeter(),
|
||||||
new EzComHitResultScore(),
|
new EzComHitResultScore(),
|
||||||
new EzComHitTiming(),
|
new EzComHitTiming(),
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
|
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
|
||||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||||
|
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||||
|
|
||||||
if (combo != null)
|
if (combo != null)
|
||||||
{
|
{
|
||||||
@@ -112,10 +113,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
spectatorList.Origin = Anchor.BottomLeft;
|
spectatorList.Origin = Anchor.BottomLeft;
|
||||||
spectatorList.Position = new Vector2(10, -10);
|
spectatorList.Position = new Vector2(10, -10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (leaderboard != null)
|
||||||
|
{
|
||||||
|
leaderboard.Anchor = Anchor.CentreLeft;
|
||||||
|
leaderboard.Origin = Anchor.CentreLeft;
|
||||||
|
leaderboard.X = 10;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
new LegacyManiaComboCounter(),
|
new LegacyManiaComboCounter(),
|
||||||
new SpectatorList(),
|
new SpectatorList(),
|
||||||
|
new DrawableGameplayLeaderboard(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.SbI
|
|||||||
combo1.Origin = Anchor.Centre;
|
combo1.Origin = Anchor.Centre;
|
||||||
combo1.Y = 200;
|
combo1.Y = 200;
|
||||||
combo1.Effect.Value = EffectType.None;
|
combo1.Effect.Value = EffectType.None;
|
||||||
combo1.NameDropdown.Value = OffsetNumberName.EZ2AC_EVOLVE;
|
combo1.NameDropdown.Value = (OffsetNumberName)43;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hitErrorMeter = container.OfType<BarHitErrorMeter>().FirstOrDefault();
|
var hitErrorMeter = container.OfType<BarHitErrorMeter>().FirstOrDefault();
|
||||||
|
|||||||
81
osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs
Normal file
81
osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Storyboards;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
public partial class TestSceneReplayRecording : PlayerTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private AudioManager audioManager { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
StartTime = 0,
|
||||||
|
},
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
StartTime = 5000,
|
||||||
|
},
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
StartTime = 10000,
|
||||||
|
},
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
StartTime = 15000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
|
||||||
|
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRecording()
|
||||||
|
{
|
||||||
|
seekTo(0);
|
||||||
|
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
|
||||||
|
AddStep("press X", () => InputManager.Key(Key.X));
|
||||||
|
AddAssert("right button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.RightButton])));
|
||||||
|
|
||||||
|
seekTo(5000);
|
||||||
|
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
|
||||||
|
AddStep("press Z", () => InputManager.Key(Key.Z));
|
||||||
|
AddAssert("left button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.LeftButton])));
|
||||||
|
|
||||||
|
seekTo(10000);
|
||||||
|
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
|
||||||
|
AddStep("press C", () => InputManager.Key(Key.C));
|
||||||
|
AddAssert("smoke button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.Smoke])));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seekTo(double time)
|
||||||
|
{
|
||||||
|
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
|
||||||
|
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -275,6 +275,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1;
|
double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1;
|
||||||
|
// do not allow the slider to extend beyond the path's calculated distance.
|
||||||
|
// this can happen in two specific circumstances:
|
||||||
|
// - floating point issues (`minDistance` is just ever so slightly larger than the calculated distance)
|
||||||
|
// - the slider was placed with a higher beat snap active than the current one,
|
||||||
|
// therefore snapping it to the current beat snap distance would mean extrapolating it beyond its actual shape as defined by its control points
|
||||||
|
minDistance = Math.Min(minDistance, HitObject.Path.CalculatedDistance);
|
||||||
|
|
||||||
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
|
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
|
||||||
proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance;
|
proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance;
|
||||||
proposedDistance = Math.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
proposedDistance = Math.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||||
|
|||||||
@@ -259,6 +259,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition), fixedTime);
|
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition), fixedTime);
|
||||||
|
|
||||||
|
if (pos.X < 0 || pos.X > OsuPlayfield.BASE_SIZE.X || pos.Y < 0 || pos.Y > OsuPlayfield.BASE_SIZE.Y)
|
||||||
|
return null;
|
||||||
|
|
||||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield);
|
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,16 @@
|
|||||||
|
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||||
|
using osu.Game.Skinning;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||||
{
|
{
|
||||||
public partial class ArgonSliderBody : PlaySliderBody
|
public partial class ArgonSliderBody : PlaySliderBody
|
||||||
{
|
{
|
||||||
|
// Eventually this would be a user setting.
|
||||||
|
public float BodyAlpha { get; init; } = 1;
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
const float path_radius = ArgonMainCirclePiece.OUTER_GRADIENT_SIZE / 2;
|
const float path_radius = ArgonMainCirclePiece.OUTER_GRADIENT_SIZE / 2;
|
||||||
@@ -26,6 +30,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
|
|
||||||
protected override Default.DrawableSliderPath CreateSliderPath() => new DrawableSliderPath();
|
protected override Default.DrawableSliderPath CreateSliderPath() => new DrawableSliderPath();
|
||||||
|
|
||||||
|
protected override Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour)
|
||||||
|
{
|
||||||
|
return base.GetBodyAccentColour(skin, hitObjectAccentColour).Opacity(BodyAlpha);
|
||||||
|
}
|
||||||
|
|
||||||
private partial class DrawableSliderPath : Default.DrawableSliderPath
|
private partial class DrawableSliderPath : Default.DrawableSliderPath
|
||||||
{
|
{
|
||||||
protected override Color4 ColourAt(float position)
|
protected override Color4 ColourAt(float position)
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
|
|
||||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||||
{
|
{
|
||||||
|
bool isPro = Skin is ArgonProSkin;
|
||||||
|
|
||||||
switch (lookup)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
case SkinComponentLookup<HitResult> resultComponent:
|
case SkinComponentLookup<HitResult> resultComponent:
|
||||||
HitResult result = resultComponent.Component;
|
HitResult result = resultComponent.Component;
|
||||||
|
|
||||||
// This should eventually be moved to a skin setting, when supported.
|
// This should eventually be moved to a skin setting, when supported.
|
||||||
if (Skin is ArgonProSkin && (result == HitResult.Great || result == HitResult.Perfect))
|
if (isPro && (result == HitResult.Great || result == HitResult.Perfect))
|
||||||
return Drawable.Empty();
|
return Drawable.Empty();
|
||||||
|
|
||||||
switch (result)
|
switch (result)
|
||||||
@@ -46,7 +48,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
return new ArgonMainCirclePiece(false);
|
return new ArgonMainCirclePiece(false);
|
||||||
|
|
||||||
case OsuSkinComponents.SliderBody:
|
case OsuSkinComponents.SliderBody:
|
||||||
return new ArgonSliderBody();
|
return new ArgonSliderBody
|
||||||
|
{
|
||||||
|
BodyAlpha = isPro ? 0.92f : 0.98f
|
||||||
|
};
|
||||||
|
|
||||||
case OsuSkinComponents.SliderBall:
|
case OsuSkinComponents.SliderBall:
|
||||||
return new ArgonSliderBall();
|
return new ArgonSliderBall();
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
|
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
|
||||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||||
|
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||||
|
|
||||||
Vector2 pos = new Vector2();
|
Vector2 pos = new Vector2();
|
||||||
|
|
||||||
@@ -89,6 +90,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
spectatorList.Anchor = Anchor.BottomLeft;
|
spectatorList.Anchor = Anchor.BottomLeft;
|
||||||
spectatorList.Origin = Anchor.BottomLeft;
|
spectatorList.Origin = Anchor.BottomLeft;
|
||||||
spectatorList.Position = pos;
|
spectatorList.Position = pos;
|
||||||
|
|
||||||
|
// maximum height of the spectator list is around ~172 units
|
||||||
|
pos += new Vector2(0, -185);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leaderboard != null)
|
||||||
|
{
|
||||||
|
leaderboard.Anchor = Anchor.BottomLeft;
|
||||||
|
leaderboard.Origin = Anchor.BottomLeft;
|
||||||
|
leaderboard.Position = pos;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
@@ -97,6 +108,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
new LegacyDefaultComboCounter(),
|
new LegacyDefaultComboCounter(),
|
||||||
new LegacyKeyCounterDisplay(),
|
new LegacyKeyCounterDisplay(),
|
||||||
new SpectatorList(),
|
new SpectatorList(),
|
||||||
|
new DrawableGameplayLeaderboard(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Replays;
|
||||||
|
using osu.Game.Rulesets.Taiko.Mods;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.Replays;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||||
|
{
|
||||||
|
public partial class TestSceneTaikoModSimplifiedRhythm : TaikoModTestScene
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestOneThirdConversion()
|
||||||
|
{
|
||||||
|
CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new TaikoModSimplifiedRhythm
|
||||||
|
{
|
||||||
|
OneThirdConversion = { Value = true },
|
||||||
|
},
|
||||||
|
Autoplay = false,
|
||||||
|
CreateBeatmap = () => new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Hit { StartTime = 1000, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 1500, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 2000, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 2333, Type = HitType.Rim }, // mod removes this
|
||||||
|
new Hit { StartTime = 2666, Type = HitType.Centre }, // mod moves this to 2500
|
||||||
|
new Hit { StartTime = 3000, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 3500, Type = HitType.Centre },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(1200),
|
||||||
|
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(1700),
|
||||||
|
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(2200),
|
||||||
|
new TaikoReplayFrame(2500, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(2700),
|
||||||
|
new TaikoReplayFrame(3000, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(3200),
|
||||||
|
new TaikoReplayFrame(3500, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(3700),
|
||||||
|
},
|
||||||
|
PassCondition = () => Player.ScoreProcessor.Combo.Value == 6 && Player.ScoreProcessor.Accuracy.Value == 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOneSixthConversion() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new TaikoModSimplifiedRhythm
|
||||||
|
{
|
||||||
|
OneSixthConversion = { Value = true }
|
||||||
|
},
|
||||||
|
Autoplay = false,
|
||||||
|
CreateBeatmap = () => new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Hit { StartTime = 1000, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 1250, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 1500, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 1666, Type = HitType.Rim }, // mod removes this
|
||||||
|
new Hit { StartTime = 1833, Type = HitType.Centre }, // mod moves this to 1750
|
||||||
|
new Hit { StartTime = 2000, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 2250, Type = HitType.Centre },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(1200),
|
||||||
|
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(1450),
|
||||||
|
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(1600),
|
||||||
|
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(1800),
|
||||||
|
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(2200),
|
||||||
|
new TaikoReplayFrame(2250, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(2450),
|
||||||
|
},
|
||||||
|
PassCondition = () => Player.ScoreProcessor.Combo.Value == 6 && Player.ScoreProcessor.Accuracy.Value == 1
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOneEighthConversion() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new TaikoModSimplifiedRhythm
|
||||||
|
{
|
||||||
|
OneEighthConversion = { Value = true }
|
||||||
|
},
|
||||||
|
Autoplay = false,
|
||||||
|
CreateBeatmap = () =>
|
||||||
|
{
|
||||||
|
const double one_eighth_timing = 125;
|
||||||
|
|
||||||
|
return new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Hit { StartTime = 1000, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 1250, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 1500, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 1500 + one_eighth_timing * 1, Type = HitType.Rim }, // mod removes this
|
||||||
|
new Hit { StartTime = 1500 + one_eighth_timing * 2 },
|
||||||
|
new Hit { StartTime = 2000, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 2000 + one_eighth_timing * 1, Type = HitType.Centre }, // mod removes this
|
||||||
|
new Hit { StartTime = 2000 + one_eighth_timing * 2, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 2000 + one_eighth_timing * 3, Type = HitType.Centre }, // mod removes this
|
||||||
|
new Hit { StartTime = 2000 + one_eighth_timing * 4, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 2000 + one_eighth_timing * 5, Type = HitType.Centre }, // mod removes this
|
||||||
|
new Hit { StartTime = 2000 + one_eighth_timing * 6, Type = HitType.Centre },
|
||||||
|
new Hit { StartTime = 2000 + one_eighth_timing * 7, Type = HitType.Centre }, // mod removes this
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(1000),
|
||||||
|
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(1250),
|
||||||
|
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(1500),
|
||||||
|
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(1750),
|
||||||
|
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(2000),
|
||||||
|
new TaikoReplayFrame(2250, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(2250),
|
||||||
|
new TaikoReplayFrame(2500, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(2500),
|
||||||
|
new TaikoReplayFrame(2750, TaikoAction.LeftCentre),
|
||||||
|
new TaikoReplayFrame(2750),
|
||||||
|
},
|
||||||
|
PassCondition = () => Player.ScoreProcessor.Combo.Value == 8 && Player.ScoreProcessor.Accuracy.Value == 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
65
osu.Game.Rulesets.Taiko.Tests/TestSceneReplayRecording.cs
Normal file
65
osu.Game.Rulesets.Taiko.Tests/TestSceneReplayRecording.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.Replays;
|
||||||
|
using osu.Game.Storyboards;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Tests
|
||||||
|
{
|
||||||
|
public partial class TestSceneReplayRecording : PlayerTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private AudioManager audioManager { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new Hit { StartTime = 0, },
|
||||||
|
new Hit { StartTime = 5000, },
|
||||||
|
new Hit { StartTime = 10000, },
|
||||||
|
new Hit { StartTime = 15000, }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
|
||||||
|
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRecording()
|
||||||
|
{
|
||||||
|
seekTo(0);
|
||||||
|
AddStep("press D", () => InputManager.Key(Key.D));
|
||||||
|
AddAssert("left rim press recorded to replay", () => Player.Score.Replay.Frames.OfType<TaikoReplayFrame>().Any(f => f.Actions.SequenceEqual([TaikoAction.LeftRim])));
|
||||||
|
|
||||||
|
seekTo(5000);
|
||||||
|
AddStep("press F", () => InputManager.Key(Key.F));
|
||||||
|
AddAssert("left centre press recorded to replay", () => Player.Score.Replay.Frames.OfType<TaikoReplayFrame>().Any(f => f.Actions.SequenceEqual([TaikoAction.LeftCentre])));
|
||||||
|
|
||||||
|
seekTo(10000);
|
||||||
|
AddStep("press J", () => InputManager.Key(Key.J));
|
||||||
|
AddAssert("right centre press recorded to replay", () => Player.Score.Replay.Frames.OfType<TaikoReplayFrame>().Any(f => f.Actions.SequenceEqual([TaikoAction.RightCentre])));
|
||||||
|
|
||||||
|
seekTo(10000);
|
||||||
|
AddStep("press K", () => InputManager.Key(Key.K));
|
||||||
|
AddAssert("right rim press recorded to replay", () => Player.Score.Replay.Frames.OfType<TaikoReplayFrame>().Any(f => f.Actions.SequenceEqual([TaikoAction.RightRim])));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seekTo(double time)
|
||||||
|
{
|
||||||
|
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
|
||||||
|
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
130
osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs
Normal file
130
osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
// 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.Framework.Bindables;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Mods
|
||||||
|
{
|
||||||
|
public class TaikoModSimplifiedRhythm : Mod, IApplicableToBeatmap
|
||||||
|
{
|
||||||
|
public override string Name => "Simplified Rhythm";
|
||||||
|
public override string Acronym => "SR";
|
||||||
|
public override double ScoreMultiplier => 0.6;
|
||||||
|
public override LocalisableString Description => "Simplify tricky rhythms!";
|
||||||
|
public override ModType Type => ModType.DifficultyReduction;
|
||||||
|
|
||||||
|
[SettingSource("1/3 to 1/2 conversion", "Converts 1/3 patterns to 1/2 rhythm.")]
|
||||||
|
public Bindable<bool> OneThirdConversion { get; } = new BindableBool();
|
||||||
|
|
||||||
|
[SettingSource("1/6 to 1/4 conversion", "Converts 1/6 patterns to 1/4 rhythm.")]
|
||||||
|
public Bindable<bool> OneSixthConversion { get; } = new BindableBool(true);
|
||||||
|
|
||||||
|
[SettingSource("1/8 to 1/4 conversion", "Converts 1/8 patterns to 1/4 rhythm.")]
|
||||||
|
public Bindable<bool> OneEighthConversion { get; } = new BindableBool();
|
||||||
|
|
||||||
|
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
var taikoBeatmap = (TaikoBeatmap)beatmap;
|
||||||
|
var controlPointInfo = taikoBeatmap.ControlPointInfo;
|
||||||
|
|
||||||
|
Hit[] hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast<Hit>().ToArray();
|
||||||
|
|
||||||
|
if (hits.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var conversions = new List<(int, int)>();
|
||||||
|
|
||||||
|
if (OneEighthConversion.Value) conversions.Add((8, 4));
|
||||||
|
if (OneSixthConversion.Value) conversions.Add((6, 4));
|
||||||
|
if (OneThirdConversion.Value) conversions.Add((3, 2));
|
||||||
|
|
||||||
|
bool inPattern = false;
|
||||||
|
|
||||||
|
foreach ((int baseRhythm, int adjustedRhythm) in conversions)
|
||||||
|
{
|
||||||
|
int patternStartIndex = 0;
|
||||||
|
|
||||||
|
for (int i = 1; i < hits.Length; i++)
|
||||||
|
{
|
||||||
|
double snapValue = getSnapBetweenNotes(controlPointInfo, hits[i - 1], hits[i]);
|
||||||
|
|
||||||
|
if (inPattern)
|
||||||
|
{
|
||||||
|
// pattern continues
|
||||||
|
if (snapValue == baseRhythm) continue;
|
||||||
|
|
||||||
|
inPattern = false;
|
||||||
|
|
||||||
|
processPattern(i);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (snapValue == baseRhythm)
|
||||||
|
{
|
||||||
|
patternStartIndex = i - 1;
|
||||||
|
inPattern = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the last pattern if we reached the end of the beatmap and are still in a pattern.
|
||||||
|
if (inPattern)
|
||||||
|
processPattern(hits.Length);
|
||||||
|
|
||||||
|
void processPattern(int patternEndIndex)
|
||||||
|
{
|
||||||
|
// Iterate through the pattern
|
||||||
|
for (int j = patternStartIndex; j < patternEndIndex; j++)
|
||||||
|
{
|
||||||
|
int indexInPattern = j - patternStartIndex;
|
||||||
|
|
||||||
|
switch (baseRhythm)
|
||||||
|
{
|
||||||
|
// 1/8: Remove every second note
|
||||||
|
case 8:
|
||||||
|
{
|
||||||
|
if (indexInPattern % 2 == 1)
|
||||||
|
{
|
||||||
|
taikoBeatmap.HitObjects.Remove(hits[j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1/6 and 1/3: Remove every second note and adjust time of every third
|
||||||
|
case 6:
|
||||||
|
case 3:
|
||||||
|
{
|
||||||
|
if (indexInPattern % 3 == 1)
|
||||||
|
taikoBeatmap.HitObjects.Remove(hits[j]);
|
||||||
|
else if (indexInPattern % 3 == 2)
|
||||||
|
hits[j].StartTime = hits[j + 1].StartTime - controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / adjustedRhythm;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(baseRhythm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getSnapBetweenNotes(ControlPointInfo controlPointInfo, Hit currentNote, Hit nextNote)
|
||||||
|
{
|
||||||
|
var currentTimingPoint = controlPointInfo.TimingPointAt(currentNote.StartTime);
|
||||||
|
return controlPointInfo.GetClosestBeatDivisor(currentTimingPoint.Time + (nextNote.StartTime - currentNote.StartTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
|||||||
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
|
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
|
||||||
periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
|
periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
|
||||||
|
|
||||||
static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
|
static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
nonGameplayPeriods = new PeriodTracker(periods);
|
nonGameplayPeriods = new PeriodTracker(periods);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||||
{
|
{
|
||||||
@@ -18,6 +21,59 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
|||||||
{
|
{
|
||||||
switch (lookup)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
|
case GlobalSkinnableContainerLookup containerLookup:
|
||||||
|
// Only handle per ruleset defaults here.
|
||||||
|
if (containerLookup.Ruleset == null)
|
||||||
|
return base.GetDrawableComponent(lookup);
|
||||||
|
|
||||||
|
switch (containerLookup.Lookup)
|
||||||
|
{
|
||||||
|
case GlobalSkinnableContainers.MainHUDComponents:
|
||||||
|
return new DefaultSkinComponentsContainer(container =>
|
||||||
|
{
|
||||||
|
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||||
|
var comboCounter = container.OfType<ArgonComboCounter>().FirstOrDefault();
|
||||||
|
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (leaderboard != null)
|
||||||
|
{
|
||||||
|
leaderboard.Anchor = leaderboard.Origin = Anchor.BottomLeft;
|
||||||
|
leaderboard.Position = new Vector2(36, -140);
|
||||||
|
leaderboard.Height = 140;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comboCounter != null)
|
||||||
|
comboCounter.Position = new Vector2(36, -66);
|
||||||
|
|
||||||
|
if (spectatorList != null)
|
||||||
|
{
|
||||||
|
spectatorList.Position = new Vector2(320, -280);
|
||||||
|
spectatorList.Anchor = Anchor.BottomLeft;
|
||||||
|
spectatorList.Origin = Anchor.TopLeft;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new DrawableGameplayLeaderboard(),
|
||||||
|
new ArgonComboCounter
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
Scale = new Vector2(1.3f),
|
||||||
|
},
|
||||||
|
new SpectatorList
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
case SkinComponentLookup<HitResult> resultComponent:
|
case SkinComponentLookup<HitResult> resultComponent:
|
||||||
// This should eventually be moved to a skin setting, when supported.
|
// This should eventually be moved to a skin setting, when supported.
|
||||||
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
|
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Skinning.Default
|
||||||
|
{
|
||||||
|
public class TaikoTrianglesSkinTransformer : SkinTransformer
|
||||||
|
{
|
||||||
|
public TaikoTrianglesSkinTransformer(ISkin skin)
|
||||||
|
: base(skin)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||||
|
{
|
||||||
|
switch (lookup)
|
||||||
|
{
|
||||||
|
case GlobalSkinnableContainerLookup containerLookup:
|
||||||
|
{
|
||||||
|
// Only handle per ruleset defaults here.
|
||||||
|
if (containerLookup.Ruleset == null)
|
||||||
|
return base.GetDrawableComponent(lookup);
|
||||||
|
|
||||||
|
switch (containerLookup.Lookup)
|
||||||
|
{
|
||||||
|
case GlobalSkinnableContainers.MainHUDComponents:
|
||||||
|
return new DefaultSkinComponentsContainer(container =>
|
||||||
|
{
|
||||||
|
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||||
|
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (leaderboard != null)
|
||||||
|
{
|
||||||
|
leaderboard.Position = new Vector2(40, -100);
|
||||||
|
leaderboard.Height = 180;
|
||||||
|
leaderboard.Anchor = Anchor.BottomLeft;
|
||||||
|
leaderboard.Origin = Anchor.BottomLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spectatorList != null)
|
||||||
|
{
|
||||||
|
spectatorList.HeaderFont.Value = Typeface.Venera;
|
||||||
|
spectatorList.HeaderColour.Value = new OsuColour().BlueLighter;
|
||||||
|
spectatorList.Anchor = Anchor.BottomLeft;
|
||||||
|
spectatorList.Origin = Anchor.TopLeft;
|
||||||
|
spectatorList.Position = new Vector2(320, -280);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new DrawableGameplayLeaderboard(),
|
||||||
|
new SpectatorList
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.GetDrawableComponent(lookup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,15 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Audio.Sample;
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.Taiko.UI;
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||||
{
|
{
|
||||||
@@ -29,119 +32,180 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
|
|
||||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||||
{
|
{
|
||||||
if (lookup is SkinComponentLookup<HitResult>)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
// if a taiko skin is providing explosion sprites, hide the judgements completely
|
case GlobalSkinnableContainerLookup containerLookup:
|
||||||
if (hasExplosion.Value)
|
|
||||||
return Drawable.Empty().With(d => d.Expire());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lookup is TaikoSkinComponentLookup taikoComponent)
|
|
||||||
{
|
|
||||||
switch (taikoComponent.Component)
|
|
||||||
{
|
{
|
||||||
case TaikoSkinComponents.DrumRollBody:
|
// Modifications for global components.
|
||||||
if (GetTexture("taiko-roll-middle") != null)
|
if (containerLookup.Ruleset == null)
|
||||||
return new LegacyDrumRoll();
|
return base.GetDrawableComponent(lookup);
|
||||||
|
|
||||||
|
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
|
||||||
|
if (!IsProvidingLegacyResources)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
case TaikoSkinComponents.InputDrum:
|
switch (containerLookup.Lookup)
|
||||||
if (hasBarLeft)
|
{
|
||||||
return new LegacyInputDrum();
|
case GlobalSkinnableContainers.MainHUDComponents:
|
||||||
|
return new DefaultSkinComponentsContainer(container =>
|
||||||
|
{
|
||||||
|
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
|
||||||
|
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||||
|
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||||
|
|
||||||
return null;
|
Vector2 pos = new Vector2();
|
||||||
|
|
||||||
case TaikoSkinComponents.DrumSamplePlayer:
|
if (combo != null)
|
||||||
return null;
|
{
|
||||||
|
combo.Anchor = Anchor.BottomLeft;
|
||||||
|
combo.Origin = Anchor.BottomLeft;
|
||||||
|
combo.Scale = new Vector2(1.28f);
|
||||||
|
|
||||||
case TaikoSkinComponents.CentreHit:
|
pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X);
|
||||||
case TaikoSkinComponents.RimHit:
|
}
|
||||||
if (hasHitCircle)
|
|
||||||
return new LegacyHit(taikoComponent.Component);
|
|
||||||
|
|
||||||
return null;
|
if (leaderboard != null)
|
||||||
|
{
|
||||||
|
leaderboard.Anchor = Anchor.BottomLeft;
|
||||||
|
leaderboard.Origin = Anchor.BottomLeft;
|
||||||
|
leaderboard.Position = pos;
|
||||||
|
leaderboard.Height = 170;
|
||||||
|
pos += new Vector2(10 + leaderboard.Width, -leaderboard.Height);
|
||||||
|
}
|
||||||
|
|
||||||
case TaikoSkinComponents.DrumRollTick:
|
if (spectatorList != null)
|
||||||
return this.GetAnimation("sliderscorepoint", false, false);
|
{
|
||||||
|
spectatorList.Anchor = Anchor.BottomLeft;
|
||||||
|
spectatorList.Origin = Anchor.TopLeft;
|
||||||
|
spectatorList.Position = pos;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
new LegacyDefaultComboCounter(),
|
||||||
|
new SpectatorList(),
|
||||||
|
new DrawableGameplayLeaderboard(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case TaikoSkinComponents.Swell:
|
return null;
|
||||||
if (GetTexture("spinner-circle") != null)
|
}
|
||||||
return new LegacySwell();
|
|
||||||
|
|
||||||
return null;
|
case SkinComponentLookup<HitResult>:
|
||||||
|
{
|
||||||
|
// if a taiko skin is providing explosion sprites, hide the judgements completely
|
||||||
|
if (hasExplosion.Value)
|
||||||
|
return Drawable.Empty().With(d => d.Expire());
|
||||||
|
|
||||||
case TaikoSkinComponents.HitTarget:
|
break;
|
||||||
if (GetTexture("taikobigcircle") != null)
|
}
|
||||||
return new TaikoLegacyHitTarget();
|
|
||||||
|
|
||||||
return null;
|
case TaikoSkinComponentLookup taikoComponent:
|
||||||
|
{
|
||||||
|
switch (taikoComponent.Component)
|
||||||
|
{
|
||||||
|
case TaikoSkinComponents.DrumRollBody:
|
||||||
|
if (GetTexture("taiko-roll-middle") != null)
|
||||||
|
return new LegacyDrumRoll();
|
||||||
|
|
||||||
case TaikoSkinComponents.PlayfieldBackgroundRight:
|
return null;
|
||||||
if (GetTexture("taiko-bar-right") != null)
|
|
||||||
return new TaikoLegacyPlayfieldBackgroundRight();
|
|
||||||
|
|
||||||
return null;
|
case TaikoSkinComponents.InputDrum:
|
||||||
|
if (hasBarLeft)
|
||||||
|
return new LegacyInputDrum();
|
||||||
|
|
||||||
case TaikoSkinComponents.PlayfieldBackgroundLeft:
|
return null;
|
||||||
// This is displayed inside LegacyInputDrum. It is required to be there for layout purposes (can be seen on legacy skins).
|
|
||||||
if (GetTexture("taiko-bar-right") != null)
|
|
||||||
return Drawable.Empty();
|
|
||||||
|
|
||||||
return null;
|
case TaikoSkinComponents.DrumSamplePlayer:
|
||||||
|
return null;
|
||||||
|
|
||||||
case TaikoSkinComponents.BarLine:
|
case TaikoSkinComponents.CentreHit:
|
||||||
if (GetTexture("taiko-barline") != null)
|
case TaikoSkinComponents.RimHit:
|
||||||
return new LegacyBarLine();
|
if (hasHitCircle)
|
||||||
|
return new LegacyHit(taikoComponent.Component);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
case TaikoSkinComponents.TaikoExplosionMiss:
|
case TaikoSkinComponents.DrumRollTick:
|
||||||
var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
|
return this.GetAnimation("sliderscorepoint", false, false);
|
||||||
if (missSprite != null)
|
|
||||||
return new LegacyHitExplosion(missSprite);
|
|
||||||
|
|
||||||
return null;
|
case TaikoSkinComponents.Swell:
|
||||||
|
if (GetTexture("spinner-circle") != null)
|
||||||
|
return new LegacySwell();
|
||||||
|
|
||||||
case TaikoSkinComponents.TaikoExplosionOk:
|
return null;
|
||||||
case TaikoSkinComponents.TaikoExplosionGreat:
|
|
||||||
string hitName = getHitName(taikoComponent.Component);
|
|
||||||
var hitSprite = this.GetAnimation(hitName, true, false);
|
|
||||||
|
|
||||||
if (hitSprite != null)
|
case TaikoSkinComponents.HitTarget:
|
||||||
{
|
if (GetTexture("taikobigcircle") != null)
|
||||||
var strongHitSprite = this.GetAnimation($"{hitName}k", true, false);
|
return new TaikoLegacyHitTarget();
|
||||||
|
|
||||||
return new LegacyHitExplosion(hitSprite, strongHitSprite);
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
case TaikoSkinComponents.PlayfieldBackgroundRight:
|
||||||
|
if (GetTexture("taiko-bar-right") != null)
|
||||||
|
return new TaikoLegacyPlayfieldBackgroundRight();
|
||||||
|
|
||||||
case TaikoSkinComponents.TaikoExplosionKiai:
|
return null;
|
||||||
// suppress the default kiai explosion if the skin brings its own sprites.
|
|
||||||
// the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield.
|
|
||||||
if (hasExplosion.Value)
|
|
||||||
return Drawable.Empty().With(d => d.Expire());
|
|
||||||
|
|
||||||
return null;
|
case TaikoSkinComponents.PlayfieldBackgroundLeft:
|
||||||
|
// This is displayed inside LegacyInputDrum. It is required to be there for layout purposes (can be seen on legacy skins).
|
||||||
|
if (GetTexture("taiko-bar-right") != null)
|
||||||
|
return Drawable.Empty();
|
||||||
|
|
||||||
case TaikoSkinComponents.Scroller:
|
return null;
|
||||||
if (GetTexture("taiko-slider") != null)
|
|
||||||
return new LegacyTaikoScroller();
|
|
||||||
|
|
||||||
return null;
|
case TaikoSkinComponents.BarLine:
|
||||||
|
if (GetTexture("taiko-barline") != null)
|
||||||
|
return new LegacyBarLine();
|
||||||
|
|
||||||
case TaikoSkinComponents.Mascot:
|
return null;
|
||||||
return new DrawableTaikoMascot();
|
|
||||||
|
|
||||||
case TaikoSkinComponents.KiaiGlow:
|
case TaikoSkinComponents.TaikoExplosionMiss:
|
||||||
if (GetTexture("taiko-glow") != null)
|
var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
|
||||||
return new LegacyKiaiGlow();
|
if (missSprite != null)
|
||||||
|
return new LegacyHitExplosion(missSprite);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
default:
|
case TaikoSkinComponents.TaikoExplosionOk:
|
||||||
throw new UnsupportedSkinComponentException(lookup);
|
case TaikoSkinComponents.TaikoExplosionGreat:
|
||||||
|
string hitName = getHitName(taikoComponent.Component);
|
||||||
|
var hitSprite = this.GetAnimation(hitName, true, false);
|
||||||
|
|
||||||
|
if (hitSprite != null)
|
||||||
|
{
|
||||||
|
var strongHitSprite = this.GetAnimation($"{hitName}k", true, false);
|
||||||
|
|
||||||
|
return new LegacyHitExplosion(hitSprite, strongHitSprite);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case TaikoSkinComponents.TaikoExplosionKiai:
|
||||||
|
// suppress the default kiai explosion if the skin brings its own sprites.
|
||||||
|
// the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield.
|
||||||
|
if (hasExplosion.Value)
|
||||||
|
return Drawable.Empty().With(d => d.Expire());
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case TaikoSkinComponents.Scroller:
|
||||||
|
if (GetTexture("taiko-slider") != null)
|
||||||
|
return new LegacyTaikoScroller();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case TaikoSkinComponents.Mascot:
|
||||||
|
return new DrawableTaikoMascot();
|
||||||
|
|
||||||
|
case TaikoSkinComponents.KiaiGlow:
|
||||||
|
if (GetTexture("taiko-glow") != null)
|
||||||
|
return new LegacyKiaiGlow();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new UnsupportedSkinComponentException(lookup);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ using osu.Game.Configuration;
|
|||||||
using osu.Game.Rulesets.Scoring.Legacy;
|
using osu.Game.Rulesets.Scoring.Legacy;
|
||||||
using osu.Game.Rulesets.Taiko.Configuration;
|
using osu.Game.Rulesets.Taiko.Configuration;
|
||||||
using osu.Game.Rulesets.Taiko.Edit.Setup;
|
using osu.Game.Rulesets.Taiko.Edit.Setup;
|
||||||
|
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||||
using osu.Game.Screens.Edit.Setup;
|
using osu.Game.Screens.Edit.Setup;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko
|
namespace osu.Game.Rulesets.Taiko
|
||||||
@@ -57,6 +58,9 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
case ArgonSkin:
|
case ArgonSkin:
|
||||||
return new TaikoArgonSkinTransformer(skin);
|
return new TaikoArgonSkinTransformer(skin);
|
||||||
|
|
||||||
|
case TrianglesSkin:
|
||||||
|
return new TaikoTrianglesSkinTransformer(skin);
|
||||||
|
|
||||||
case LegacySkin:
|
case LegacySkin:
|
||||||
return new TaikoLegacySkinTransformer(skin);
|
return new TaikoLegacySkinTransformer(skin);
|
||||||
}
|
}
|
||||||
@@ -130,6 +134,7 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
new TaikoModEasy(),
|
new TaikoModEasy(),
|
||||||
new TaikoModNoFail(),
|
new TaikoModNoFail(),
|
||||||
new MultiMod(new TaikoModHalfTime(), new TaikoModDaycore()),
|
new MultiMod(new TaikoModHalfTime(), new TaikoModDaycore()),
|
||||||
|
new TaikoModSimplifiedRhythm(),
|
||||||
};
|
};
|
||||||
|
|
||||||
case ModType.DifficultyIncrease:
|
case ModType.DifficultyIncrease:
|
||||||
|
|||||||
BIN
osu.Game.Tests/Resources/Archives/modified-argon-20250424.osk
Normal file
BIN
osu.Game.Tests/Resources/Archives/modified-argon-20250424.osk
Normal file
Binary file not shown.
@@ -75,6 +75,8 @@ namespace osu.Game.Tests.Skins
|
|||||||
"Archives/modified-argon-20250116.osk",
|
"Archives/modified-argon-20250116.osk",
|
||||||
// Covers player team flag
|
// Covers player team flag
|
||||||
"Archives/modified-argon-20250214.osk",
|
"Archives/modified-argon-20250214.osk",
|
||||||
|
// Covers skinnable leaderboard
|
||||||
|
"Archives/modified-argon-20250424.osk",
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("toggle expanded", () =>
|
AddStep("toggle expanded", () =>
|
||||||
{
|
{
|
||||||
if (leaderboard.IsNotNull())
|
if (leaderboard.IsNotNull())
|
||||||
leaderboard.Expanded.Value = !leaderboard.Expanded.Value;
|
leaderboard.ForceExpand.Value = !leaderboard.ForceExpand.Value;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
|
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
|
||||||
@@ -50,11 +50,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("add many scores in one go", () =>
|
AddStep("add many scores in one go", () =>
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 32; i++)
|
for (int i = 0; i < 32; i++)
|
||||||
createRandomScore(new APIUser { Username = $"Player {i + 1}" });
|
leaderboardProvider.CreateRandomScore(new APIUser { Username = $"Player {i + 1}" });
|
||||||
|
|
||||||
// Add player at end to force an animation down the whole list.
|
// Add player at end to force an animation down the whole list.
|
||||||
playerScore.Value = 0;
|
playerScore.Value = 0;
|
||||||
createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
|
leaderboardProvider.CreateLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gameplay leaderboard has custom scroll logic, which when coupled with LayoutDuration
|
// Gameplay leaderboard has custom scroll logic, which when coupled with LayoutDuration
|
||||||
@@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
addLocalPlayer();
|
addLocalPlayer();
|
||||||
|
|
||||||
int playerNumber = 1;
|
int playerNumber = 1;
|
||||||
AddRepeatStep("add player with random score", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 10);
|
AddRepeatStep("add player with random score", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -93,30 +93,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
createLeaderboard();
|
createLeaderboard();
|
||||||
addLocalPlayer();
|
addLocalPlayer();
|
||||||
|
|
||||||
AddStep("add peppy", () => createRandomScore(new APIUser { Username = "peppy", Id = 2 }));
|
AddStep("add peppy", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = "peppy", Id = 2 }));
|
||||||
AddStep("add smoogipoo", () => createRandomScore(new APIUser { Username = "smoogipoo", Id = 1040328 }));
|
AddStep("add smoogipoo", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = "smoogipoo", Id = 1040328 }));
|
||||||
AddStep("add flyte", () => createRandomScore(new APIUser { Username = "flyte", Id = 3103765 }));
|
AddStep("add flyte", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = "flyte", Id = 3103765 }));
|
||||||
AddStep("add frenzibyte", () => createRandomScore(new APIUser { Username = "frenzibyte", Id = 14210502 }));
|
AddStep("add frenzibyte", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = "frenzibyte", Id = 14210502 }));
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestMaxHeight()
|
|
||||||
{
|
|
||||||
createLeaderboard();
|
|
||||||
addLocalPlayer();
|
|
||||||
|
|
||||||
int playerNumber = 1;
|
|
||||||
AddRepeatStep("add 3 other players", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 3);
|
|
||||||
checkHeight(4);
|
|
||||||
|
|
||||||
AddRepeatStep("add 4 other players", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 4);
|
|
||||||
checkHeight(8);
|
|
||||||
|
|
||||||
AddRepeatStep("add 4 other players", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 4);
|
|
||||||
checkHeight(8);
|
|
||||||
|
|
||||||
void checkHeight(int panelCount)
|
|
||||||
=> AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (DrawableGameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -143,12 +123,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
int playerNumber = 1;
|
int playerNumber = 1;
|
||||||
|
|
||||||
AddRepeatStep("add 3 other players", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 3);
|
AddRepeatStep("add 3 other players", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 3);
|
||||||
AddUntilStep("no pink color scores",
|
AddUntilStep("no pink color scores",
|
||||||
() => leaderboard.ChildrenOfType<Box>().Select(b => ((Colour4)b.Colour).ToHex()),
|
() => leaderboard.ChildrenOfType<Box>().Select(b => ((Colour4)b.Colour).ToHex()),
|
||||||
() => Does.Not.Contain("#FF549A"));
|
() => Does.Not.Contain("#FF549A"));
|
||||||
|
|
||||||
AddRepeatStep("add 3 friend score", () => createRandomScore(friend), 3);
|
AddRepeatStep("add 3 friend score", () => leaderboardProvider.CreateRandomScore(friend), 3);
|
||||||
AddUntilStep("at least one friend score is pink",
|
AddUntilStep("at least one friend score is pink",
|
||||||
() => leaderboard.GetAllScoresForUsername("my friend")
|
() => leaderboard.GetAllScoresForUsername("my friend")
|
||||||
.SelectMany(score => score.ChildrenOfType<Box>())
|
.SelectMany(score => score.ChildrenOfType<Box>())
|
||||||
@@ -161,7 +141,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("add local player", () =>
|
AddStep("add local player", () =>
|
||||||
{
|
{
|
||||||
playerScore.Value = 1222333;
|
playerScore.Value = 1222333;
|
||||||
createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
|
leaderboardProvider.CreateLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,14 +159,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createRandomScore(APIUser user) => createLeaderboardScore(new BindableLong(RNG.Next(0, 5_000_000)), user);
|
|
||||||
|
|
||||||
private void createLeaderboardScore(BindableLong score, APIUser user, bool isTracked = false)
|
|
||||||
{
|
|
||||||
var leaderboardScore = new GameplayLeaderboardScore(user, isTracked, score);
|
|
||||||
leaderboardProvider.Scores.Add(leaderboardScore);
|
|
||||||
}
|
|
||||||
|
|
||||||
private partial class TestDrawableGameplayLeaderboard : DrawableGameplayLeaderboard
|
private partial class TestDrawableGameplayLeaderboard : DrawableGameplayLeaderboard
|
||||||
{
|
{
|
||||||
public float Spacing => Flow.Spacing.Y;
|
public float Spacing => Flow.Spacing.Y;
|
||||||
@@ -195,10 +167,20 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
=> Flow.Where(i => i.User?.Username == username);
|
=> Flow.Where(i => i.User?.Username == username);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider
|
public class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider
|
||||||
{
|
{
|
||||||
IBindableList<GameplayLeaderboardScore> IGameplayLeaderboardProvider.Scores => Scores;
|
|
||||||
public BindableList<GameplayLeaderboardScore> Scores { get; } = new BindableList<GameplayLeaderboardScore>();
|
public BindableList<GameplayLeaderboardScore> Scores { get; } = new BindableList<GameplayLeaderboardScore>();
|
||||||
|
|
||||||
|
public GameplayLeaderboardScore CreateRandomScore(APIUser user) => CreateLeaderboardScore(new BindableLong(RNG.Next(0, 5_000_000)), user);
|
||||||
|
|
||||||
|
public GameplayLeaderboardScore CreateLeaderboardScore(BindableLong totalScore, APIUser user, bool isTracked = false)
|
||||||
|
{
|
||||||
|
var score = new GameplayLeaderboardScore(user, isTracked, totalScore);
|
||||||
|
Scores.Add(score);
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
IBindableList<GameplayLeaderboardScore> IGameplayLeaderboardProvider.Scores => Scores;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ using osu.Framework.Graphics.Rendering;
|
|||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Framework.IO.Stores;
|
using osu.Framework.IO.Stores;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
@@ -41,6 +43,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private RulesetStore rulesets { get; set; } = null!;
|
private RulesetStore rulesets { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuConfigManager configManager { get; set; } = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
@@ -49,6 +54,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
skins["legacy"] = new DefaultLegacySkin(this);
|
skins["legacy"] = new DefaultLegacySkin(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddToggleStep("toggle leaderboard", b => configManager.SetValue(OsuSetting.GameplayLeaderboard, b));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestLayout(
|
public void TestLayout(
|
||||||
[Values("argon", "triangles", "legacy")]
|
[Values("argon", "triangles", "legacy")]
|
||||||
|
|||||||
@@ -204,12 +204,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Direction = FillDirection.Vertical,
|
Direction = FillDirection.Vertical,
|
||||||
AutoSizeAxes = Axes.Both,
|
AutoSizeAxes = Axes.Both,
|
||||||
Children = new[]
|
ChildrenEnumerable = hitWindows?.GetAllAvailableWindows().Select(w => new OsuSpriteText { Text = $@"{w.result}: {w.length}" }) ?? []
|
||||||
{
|
|
||||||
new OsuSpriteText { Text = $@"Great: {hitWindows?.WindowFor(HitResult.Great)}" },
|
|
||||||
new OsuSpriteText { Text = $@"Good: {hitWindows?.WindowFor(HitResult.Ok)}" },
|
|
||||||
new OsuSpriteText { Text = $@"Meh: {hitWindows?.WindowFor(HitResult.Meh)}" },
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Add(new BarHitErrorMeter
|
Add(new BarHitErrorMeter
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
public void TestScoreUpdates()
|
public void TestScoreUpdates()
|
||||||
{
|
{
|
||||||
AddRepeatStep("update state", UpdateUserStatesRandomly, 100);
|
AddRepeatStep("update state", UpdateUserStatesRandomly, 100);
|
||||||
AddToggleStep("switch compact mode", expanded => Leaderboard!.Expanded.Value = expanded);
|
AddToggleStep("switch compact mode", expanded => Leaderboard!.ForceExpand.Value = expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Expanded = { Value = true }
|
ForceExpand = { Value = true }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -560,7 +560,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
|
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
|
||||||
|
|
||||||
private DrawableGameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<DrawableGameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId);
|
private DrawableGameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.Leaderboard.ChildrenOfType<DrawableGameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId);
|
||||||
|
|
||||||
private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
|
private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
Origin = Anchor.BottomCentre,
|
Origin = Anchor.BottomCentre,
|
||||||
Team1Score = { BindTarget = LeaderboardProvider.TeamScores[0] },
|
Team1Score = { BindTarget = LeaderboardProvider.TeamScores[0] },
|
||||||
Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] },
|
Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] },
|
||||||
Expanded = { BindTarget = Leaderboard!.Expanded },
|
Expanded = { BindTarget = Leaderboard!.ForceExpand },
|
||||||
}, Add);
|
}, Add);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// 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.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Screens.Select.Leaderboards;
|
||||||
|
using osu.Game.Tests.Gameplay;
|
||||||
|
using osu.Game.Tests.Visual.Gameplay;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
|
{
|
||||||
|
public partial class TestSceneMultiplayerPositionDisplay : OsuTestScene
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private OsuConfigManager config { get; set; } = null!;
|
||||||
|
|
||||||
|
private GameplayLeaderboardScore score = null!;
|
||||||
|
|
||||||
|
private readonly Bindable<int?> position = new Bindable<int?>(8);
|
||||||
|
|
||||||
|
private TestSceneGameplayLeaderboard.TestGameplayLeaderboardProvider leaderboardProvider = null!;
|
||||||
|
private MultiplayerPositionDisplay display = null!;
|
||||||
|
private GameplayState gameplayState = null!;
|
||||||
|
|
||||||
|
private const int player_count = 32;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAppearance()
|
||||||
|
{
|
||||||
|
AddStep("create content", () =>
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new DependencyProvidingContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
CachedDependencies =
|
||||||
|
[
|
||||||
|
(typeof(IGameplayLeaderboardProvider), leaderboardProvider = new TestSceneGameplayLeaderboard.TestGameplayLeaderboardProvider()),
|
||||||
|
(typeof(GameplayState), gameplayState = TestGameplayState.Create(new OsuRuleset()))
|
||||||
|
],
|
||||||
|
Child = display = new MultiplayerPositionDisplay
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
score = leaderboardProvider.CreateLeaderboardScore(new BindableLong(), API.LocalUser.Value, true);
|
||||||
|
score.Position.BindTo(position);
|
||||||
|
|
||||||
|
for (int i = 0; i < player_count - 1; i++)
|
||||||
|
{
|
||||||
|
var r = leaderboardProvider.CreateRandomScore(new APIUser());
|
||||||
|
r.Position.Value = i;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AddSliderStep("set score position", 1, player_count, position.Value!.Value, r => position.Value = r);
|
||||||
|
AddStep("unset position", () => position.Value = null);
|
||||||
|
|
||||||
|
AddStep("toggle leaderboardProvider on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true));
|
||||||
|
AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
AddStep("toggle leaderboardProvider off", () => config.SetValue(OsuSetting.GameplayLeaderboard, false));
|
||||||
|
AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0));
|
||||||
|
|
||||||
|
AddStep("enter break", () => ((Bindable<LocalUserPlayingState>)gameplayState.PlayingState).Value = LocalUserPlayingState.Break);
|
||||||
|
AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
AddStep("exit break", () => ((Bindable<LocalUserPlayingState>)gameplayState.PlayingState).Value = LocalUserPlayingState.Playing);
|
||||||
|
AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0));
|
||||||
|
|
||||||
|
AddStep("toggle leaderboardProvider on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true));
|
||||||
|
AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
AddStep("change local user", () => ((DummyAPIAccess)API).LocalUser.Value = new GuestUser());
|
||||||
|
AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Screens;
|
||||||
|
using osu.Game.Screens.Footer;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Navigation
|
||||||
|
{
|
||||||
|
public partial class TestSceneScreenFooterNavigation : OsuGameTestScene
|
||||||
|
{
|
||||||
|
private ScreenFooter screenFooter => this.ChildrenOfType<ScreenFooter>().Single();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestFooterHidesOldBackButton()
|
||||||
|
{
|
||||||
|
PushAndConfirm(() => new TestScreen(false));
|
||||||
|
AddAssert("footer hidden", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||||
|
AddAssert("old back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||||
|
|
||||||
|
PushAndConfirm(() => new TestScreen(true));
|
||||||
|
AddAssert("footer shown", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||||
|
AddAssert("old back button hidden", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||||
|
|
||||||
|
PushAndConfirm(() => new TestScreen(false));
|
||||||
|
AddAssert("footer hidden", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||||
|
AddAssert("back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||||
|
|
||||||
|
AddStep("exit screen", () => Game.ScreenStack.Exit());
|
||||||
|
AddAssert("footer shown", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||||
|
AddAssert("old back button hidden", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||||
|
|
||||||
|
AddStep("exit screen", () => Game.ScreenStack.Exit());
|
||||||
|
AddAssert("footer hidden", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||||
|
AddAssert("old back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class TestScreen : OsuScreen
|
||||||
|
{
|
||||||
|
public override bool ShowFooter { get; }
|
||||||
|
|
||||||
|
public TestScreen(bool footer)
|
||||||
|
{
|
||||||
|
ShowFooter = footer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -611,7 +611,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
|
|
||||||
AddAssert("ensure score is databased", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == false));
|
AddAssert("ensure score is databased", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == false));
|
||||||
|
|
||||||
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
|
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action!.Invoke());
|
||||||
|
|
||||||
AddStep("show local scores",
|
AddStep("show local scores",
|
||||||
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
|
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
|
||||||
@@ -644,7 +644,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
|
|
||||||
AddAssert("ensure score is databased", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == false));
|
AddAssert("ensure score is databased", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == false));
|
||||||
|
|
||||||
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
|
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action!.Invoke());
|
||||||
|
|
||||||
AddStep("show local scores",
|
AddStep("show local scores",
|
||||||
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
|
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
|
||||||
@@ -734,7 +734,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
public void TestPushSongSelectAndPressBackButtonImmediately()
|
public void TestPushSongSelectAndPressBackButtonImmediately()
|
||||||
{
|
{
|
||||||
AddStep("push song select", () => Game.ScreenStack.Push(new TestPlaySongSelect()));
|
AddStep("push song select", () => Game.ScreenStack.Push(new TestPlaySongSelect()));
|
||||||
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
|
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action!.Invoke());
|
||||||
AddWaitStep("wait two frames", 2);
|
AddWaitStep("wait two frames", 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -914,7 +914,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
|
|
||||||
AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
|
AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
|
||||||
AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open());
|
AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open());
|
||||||
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
|
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action!.Invoke());
|
||||||
AddWaitStep("wait two frames", 2);
|
AddWaitStep("wait two frames", 2);
|
||||||
|
|
||||||
AddStep("exit lounge", () => Game.ScreenStack.Exit());
|
AddStep("exit lounge", () => Game.ScreenStack.Exit());
|
||||||
|
|||||||
@@ -75,9 +75,11 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[TestCase("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", LinkAction.External)]
|
[TestCase("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", LinkAction.External)]
|
||||||
[TestCase("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", LinkAction.External, LinkAction.OpenWiki)]
|
[TestCase("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", LinkAction.External, LinkAction.OpenWiki)]
|
||||||
[TestCase("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present).")] // note that there's 0 links here (they get removed if a channel is not found)
|
[TestCase("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present).")] // note that there's 0 links here (they get removed if a channel is not found)
|
||||||
[TestCase("Join my multiplayer game osump://12346.", LinkAction.JoinMultiplayerMatch)]
|
[TestCase("Join my multiplayer game osu://room/12346.", LinkAction.JoinRoom)]
|
||||||
[TestCase("Join my multiplayer gameosump://12346.", LinkAction.JoinMultiplayerMatch)]
|
[TestCase("Join my multiplayer gameosu://room/12346.", LinkAction.JoinRoom)]
|
||||||
[TestCase("Join my [multiplayer game](osump://12346).", LinkAction.JoinMultiplayerMatch)]
|
[TestCase("Join my [multiplayer game](osu://room/12346).", LinkAction.JoinRoom)]
|
||||||
|
[TestCase("Join my multiplayer game http://dev.ppy.sh/multiplayer/rooms/12346", LinkAction.JoinRoom)]
|
||||||
|
[TestCase("Join my [multiplayer game](http://dev.ppy.sh/multiplayer/rooms/12346).", LinkAction.JoinRoom)]
|
||||||
[TestCase($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", LinkAction.OpenChannel)]
|
[TestCase($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", LinkAction.OpenChannel)]
|
||||||
[TestCase($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)]
|
[TestCase($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)]
|
||||||
[TestCase($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)]
|
[TestCase($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)]
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using osu.Game.Overlays.Comments;
|
|||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
{
|
{
|
||||||
|
[Ignore("This test hits online resources (and online retrieval can fail at any time), and also performs network calls to the production instance of the website. Un-ignore this test when it's actually actively needed.")]
|
||||||
public partial class TestSceneImageProxying : OsuTestScene
|
public partial class TestSceneImageProxying : OsuTestScene
|
||||||
{
|
{
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -167,6 +167,57 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
AddAssert("local score is #16", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16));
|
AddAssert("local score is #16", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOnlineLeaderboardWithLessThan50Scores_UserWasInTop50()
|
||||||
|
{
|
||||||
|
ScoreInfo localScore = null!;
|
||||||
|
|
||||||
|
AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null)));
|
||||||
|
AddStep("set up request handling", () => dummyAPI.HandleRequest = req =>
|
||||||
|
{
|
||||||
|
switch (req)
|
||||||
|
{
|
||||||
|
case GetScoresRequest getScoresRequest:
|
||||||
|
var scores = new List<SoloScoreInfo>();
|
||||||
|
|
||||||
|
for (int i = 0; i < 30; ++i)
|
||||||
|
{
|
||||||
|
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||||
|
score.TotalScore = 10_000 * (30 - i);
|
||||||
|
score.Position = i + 1;
|
||||||
|
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||||
|
}
|
||||||
|
|
||||||
|
scores[^1].ID = 123456;
|
||||||
|
scores[^1].UserID = API.LocalUser.Value.OnlineID;
|
||||||
|
|
||||||
|
getScoresRequest.TriggerSuccess(new APIScoresCollection
|
||||||
|
{
|
||||||
|
Scores = scores,
|
||||||
|
UserScore = new APIScoreWithPosition
|
||||||
|
{
|
||||||
|
Score = scores[^1],
|
||||||
|
Position = 30
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("show results", () =>
|
||||||
|
{
|
||||||
|
localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||||
|
localScore.TotalScore = 151_000;
|
||||||
|
localScore.Position = null;
|
||||||
|
LoadScreen(new SoloResultsScreen(localScore));
|
||||||
|
});
|
||||||
|
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||||
|
AddAssert("local score is #16", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16));
|
||||||
|
AddAssert("previous user best not shown", () => this.ChildrenOfType<ScorePanel>().All(p => p.Score.OnlineID != 123456));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestOnlineLeaderboardWithLessThan50Scores_UserIsLast()
|
public void TestOnlineLeaderboardWithLessThan50Scores_UserIsLast()
|
||||||
{
|
{
|
||||||
@@ -207,7 +258,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestOnlineLeaderboardWithMoreThan50Scores_UserOutsideOfTop50()
|
public void TestOnlineLeaderboardWithMoreThan50Scores_UserOutsideOfTop50_DidNotBeatOwnBest()
|
||||||
{
|
{
|
||||||
ScoreInfo localScore = null!;
|
ScoreInfo localScore = null!;
|
||||||
|
|
||||||
@@ -227,15 +278,69 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
scores.Add(SoloScoreInfo.ForSubmission(score));
|
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||||
}
|
}
|
||||||
|
|
||||||
var userBest = TestResources.CreateTestScoreInfo(importedBeatmap);
|
var userBest = SoloScoreInfo.ForSubmission(TestResources.CreateTestScoreInfo(importedBeatmap));
|
||||||
userBest.TotalScore = 50_000;
|
userBest.TotalScore = 50_000;
|
||||||
|
userBest.ID = 123456;
|
||||||
|
|
||||||
getScoresRequest.TriggerSuccess(new APIScoresCollection
|
getScoresRequest.TriggerSuccess(new APIScoresCollection
|
||||||
{
|
{
|
||||||
Scores = scores,
|
Scores = scores,
|
||||||
UserScore = new APIScoreWithPosition
|
UserScore = new APIScoreWithPosition
|
||||||
{
|
{
|
||||||
Score = SoloScoreInfo.ForSubmission(userBest),
|
Score = userBest,
|
||||||
|
Position = 133_337,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("show results", () =>
|
||||||
|
{
|
||||||
|
localScore = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||||
|
localScore.TotalScore = 31_000;
|
||||||
|
localScore.Position = null;
|
||||||
|
LoadScreen(new SoloResultsScreen(localScore));
|
||||||
|
});
|
||||||
|
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||||
|
AddAssert("local score has no position", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.Null);
|
||||||
|
AddAssert("previous user best shown at same position", () => this.ChildrenOfType<ScorePanel>().Any(p => p.Score.OnlineID == 123456 && p.ScorePosition.Value == 133_337));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOnlineLeaderboardWithMoreThan50Scores_UserOutsideOfTop50_BeatOwnBest()
|
||||||
|
{
|
||||||
|
ScoreInfo localScore = null!;
|
||||||
|
|
||||||
|
AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null)));
|
||||||
|
AddStep("set up request handling", () => dummyAPI.HandleRequest = req =>
|
||||||
|
{
|
||||||
|
switch (req)
|
||||||
|
{
|
||||||
|
case GetScoresRequest getScoresRequest:
|
||||||
|
var scores = new List<SoloScoreInfo>();
|
||||||
|
|
||||||
|
for (int i = 0; i < 50; ++i)
|
||||||
|
{
|
||||||
|
var score = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||||
|
score.TotalScore = 500_000 + 10_000 * (50 - i);
|
||||||
|
score.Position = i + 1;
|
||||||
|
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||||
|
}
|
||||||
|
|
||||||
|
var userBest = SoloScoreInfo.ForSubmission(TestResources.CreateTestScoreInfo(importedBeatmap));
|
||||||
|
userBest.TotalScore = 50_000;
|
||||||
|
userBest.ID = 123456;
|
||||||
|
userBest.UserID = API.LocalUser.Value.OnlineID;
|
||||||
|
|
||||||
|
getScoresRequest.TriggerSuccess(new APIScoresCollection
|
||||||
|
{
|
||||||
|
Scores = scores,
|
||||||
|
UserScore = new APIScoreWithPosition
|
||||||
|
{
|
||||||
|
Score = userBest,
|
||||||
Position = 133_337,
|
Position = 133_337,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -254,7 +359,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
});
|
});
|
||||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||||
AddAssert("local score has no position", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.Null);
|
AddAssert("local score has no position", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.Null);
|
||||||
AddAssert("user best position preserved", () => this.ChildrenOfType<ScorePanel>().Any(p => p.ScorePosition.Value == 133_337));
|
AddAssert("previous user best not shown", () => this.ChildrenOfType<ScorePanel>().All(p => p.Score.OnlineID != 123456));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -278,15 +383,17 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
scores.Add(SoloScoreInfo.ForSubmission(score));
|
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||||
}
|
}
|
||||||
|
|
||||||
var userBest = TestResources.CreateTestScoreInfo(importedBeatmap);
|
var userBest = SoloScoreInfo.ForSubmission(TestResources.CreateTestScoreInfo(importedBeatmap));
|
||||||
userBest.TotalScore = 50_000;
|
userBest.TotalScore = 50_000;
|
||||||
|
userBest.ID = 123456;
|
||||||
|
userBest.UserID = API.LocalUser.Value.OnlineID;
|
||||||
|
|
||||||
getScoresRequest.TriggerSuccess(new APIScoresCollection
|
getScoresRequest.TriggerSuccess(new APIScoresCollection
|
||||||
{
|
{
|
||||||
Scores = scores,
|
Scores = scores,
|
||||||
UserScore = new APIScoreWithPosition
|
UserScore = new APIScoreWithPosition
|
||||||
{
|
{
|
||||||
Score = SoloScoreInfo.ForSubmission(userBest),
|
Score = userBest,
|
||||||
Position = 133_337,
|
Position = 133_337,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -305,7 +412,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
});
|
});
|
||||||
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded);
|
||||||
AddAssert("local score is #36", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(36));
|
AddAssert("local score is #36", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(36));
|
||||||
AddAssert("user best position incremented by 1", () => this.ChildrenOfType<ScorePanel>().Any(p => p.ScorePosition.Value == 133_338));
|
AddAssert("previous user best not shown", () => this.ChildrenOfType<ScorePanel>().All(p => p.Score.OnlineID != 123456));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
// 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 System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.Carousel;
|
||||||
|
using osu.Game.Screens.Select;
|
||||||
|
using osu.Game.Screens.Select.Filter;
|
||||||
|
using osu.Game.Screens.SelectV2;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class BeatmapCarouselFilterSortingTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public async Task TestSorting()
|
||||||
|
{
|
||||||
|
List<BeatmapSetInfo> beatmapSets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
|
const string zzz_lowercase = "zzzzz";
|
||||||
|
const string zzz_uppercase = "ZZZZZ";
|
||||||
|
const int diff_count = 5;
|
||||||
|
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
{
|
||||||
|
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
|
|
||||||
|
if (i == 4)
|
||||||
|
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_uppercase);
|
||||||
|
|
||||||
|
if (i == 8)
|
||||||
|
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_lowercase);
|
||||||
|
|
||||||
|
if (i == 12)
|
||||||
|
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_uppercase);
|
||||||
|
|
||||||
|
if (i == 16)
|
||||||
|
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_lowercase);
|
||||||
|
|
||||||
|
beatmapSets.Add(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = await runSorting(SortMode.Author, beatmapSets);
|
||||||
|
|
||||||
|
Assert.That(results.Last().Metadata.Author.Username, Is.EqualTo(zzz_uppercase));
|
||||||
|
Assert.That(results.SkipLast(diff_count).Last().Metadata.Author.Username, Is.EqualTo(zzz_lowercase));
|
||||||
|
|
||||||
|
results = await runSorting(SortMode.Artist, beatmapSets);
|
||||||
|
|
||||||
|
Assert.That(results.Last().Metadata.Artist, Is.EqualTo(zzz_uppercase));
|
||||||
|
Assert.That(results.SkipLast(diff_count).Last().Metadata.Artist, Is.EqualTo(zzz_lowercase));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task TestSortingDateSubmitted()
|
||||||
|
{
|
||||||
|
List<BeatmapSetInfo> beatmapSets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
|
const string zzz_string = "zzzzz";
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
var set = TestResources.CreateTestBeatmapSetInfo(5);
|
||||||
|
|
||||||
|
// A total of 6 sets have date submitted (4 don't)
|
||||||
|
// A total of 5 sets have artist string (3 of which also have date submitted)
|
||||||
|
|
||||||
|
if (i >= 2 && i < 8) // i = 2, 3, 4, 5, 6, 7 have submitted date
|
||||||
|
set.DateSubmitted = DateTimeOffset.Now.AddMinutes(i);
|
||||||
|
if (i < 5) // i = 0, 1, 2, 3, 4 have matching string
|
||||||
|
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string);
|
||||||
|
|
||||||
|
set.Beatmaps.ForEach(b => b.Metadata.Title = $"submitted: {set.DateSubmitted}");
|
||||||
|
|
||||||
|
beatmapSets.Add(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = await runSorting(SortMode.DateSubmitted, beatmapSets);
|
||||||
|
|
||||||
|
Assert.That(results.Count(), Is.EqualTo(50));
|
||||||
|
|
||||||
|
Assert.That(results.Reverse().TakeWhile(b => b.BeatmapSet!.DateSubmitted == null).Count(), Is.EqualTo(20), () => "missing dates should be at the end");
|
||||||
|
Assert.That(results.TakeWhile(b => b.BeatmapSet!.DateSubmitted != null).Count(), Is.EqualTo(30), () => "non-missing dates should be at the start");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task TestSortByArtistUsesTitleAsTiebreaker()
|
||||||
|
{
|
||||||
|
List<BeatmapSetInfo> beatmapSets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
|
const int diff_count = 5;
|
||||||
|
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
{
|
||||||
|
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
|
|
||||||
|
if (i == 4)
|
||||||
|
{
|
||||||
|
set.Beatmaps.ForEach(b =>
|
||||||
|
{
|
||||||
|
b.Metadata.Artist = "ZZZ";
|
||||||
|
b.Metadata.Title = "AAA";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == 8)
|
||||||
|
{
|
||||||
|
set.Beatmaps.ForEach(b =>
|
||||||
|
{
|
||||||
|
b.Metadata.Artist = "ZZZ";
|
||||||
|
b.Metadata.Title = "ZZZ";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beatmapSets.Add(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = await runSorting(SortMode.Artist, beatmapSets);
|
||||||
|
|
||||||
|
Assert.That(() =>
|
||||||
|
{
|
||||||
|
var lastItem = results.Last();
|
||||||
|
return lastItem.Metadata.Artist == "ZZZ" && lastItem.Metadata.Title == "ZZZ";
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(() =>
|
||||||
|
{
|
||||||
|
var secondLastItem = results.SkipLast(diff_count).Last();
|
||||||
|
return secondLastItem.Metadata.Artist == "ZZZ" && secondLastItem.Metadata.Title == "AAA";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures stability is maintained on different sort modes for items with equal properties.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task TestSortingStabilityDateAdded()
|
||||||
|
{
|
||||||
|
List<BeatmapSetInfo> beatmapSets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
var set = TestResources.CreateTestBeatmapSetInfo();
|
||||||
|
|
||||||
|
set.DateAdded = DateTimeOffset.FromUnixTimeSeconds(i);
|
||||||
|
|
||||||
|
// only need to set the first as they are a shared reference.
|
||||||
|
var beatmap = set.Beatmaps.First();
|
||||||
|
|
||||||
|
beatmap.Metadata.Artist = "a";
|
||||||
|
beatmap.Metadata.Title = "b";
|
||||||
|
|
||||||
|
beatmapSets.Add(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = await runSorting(SortMode.Title, beatmapSets);
|
||||||
|
|
||||||
|
Assert.That(results.Select(b => b.BeatmapSet!.DateAdded), Is.Ordered.Descending);
|
||||||
|
|
||||||
|
results = await runSorting(SortMode.Artist, beatmapSets);
|
||||||
|
|
||||||
|
Assert.That(results.Select(b => b.BeatmapSet!.DateAdded), Is.Ordered.Descending);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IEnumerable<BeatmapInfo>> runSorting(SortMode sort, List<BeatmapSetInfo> beatmapSets)
|
||||||
|
{
|
||||||
|
var sorter = new BeatmapCarouselFilterSorting(() => new FilterCriteria { Sort = sort });
|
||||||
|
var carouselItems = await sorter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))), CancellationToken.None);
|
||||||
|
return carouselItems.Select(ci => ci.Model).OfType<BeatmapInfo>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@@ -20,6 +22,7 @@ using osu.Game.Graphics.Carousel;
|
|||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
|
using osu.Game.Screens.Select.Filter;
|
||||||
using osu.Game.Screens.SelectV2;
|
using osu.Game.Screens.SelectV2;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Beatmaps;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
@@ -34,7 +37,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
{
|
{
|
||||||
protected readonly BindableList<BeatmapSetInfo> BeatmapSets = new BindableList<BeatmapSetInfo>();
|
protected readonly BindableList<BeatmapSetInfo> BeatmapSets = new BindableList<BeatmapSetInfo>();
|
||||||
|
|
||||||
protected BeatmapCarousel Carousel = null!;
|
protected TestBeatmapCarousel Carousel = null!;
|
||||||
|
|
||||||
protected OsuScrollContainer<Drawable> Scroll => Carousel.ChildrenOfType<OsuScrollContainer<Drawable>>().Single();
|
protected OsuScrollContainer<Drawable> Scroll => Carousel.ChildrenOfType<OsuScrollContainer<Drawable>>().Single();
|
||||||
|
|
||||||
@@ -48,6 +51,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
|
|
||||||
private int beatmapCount;
|
private int beatmapCount;
|
||||||
|
|
||||||
|
protected int NewItemsPresentedInvocationCount;
|
||||||
|
|
||||||
protected BeatmapCarouselTestScene()
|
protected BeatmapCarouselTestScene()
|
||||||
{
|
{
|
||||||
store = new TestBeatmapStore
|
store = new TestBeatmapStore
|
||||||
@@ -64,6 +69,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
{
|
{
|
||||||
AddStep("create components", () =>
|
AddStep("create components", () =>
|
||||||
{
|
{
|
||||||
|
NewItemsPresentedInvocationCount = 0;
|
||||||
|
|
||||||
Box topBox;
|
Box topBox;
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
@@ -95,8 +102,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
},
|
},
|
||||||
new Drawable[]
|
new Drawable[]
|
||||||
{
|
{
|
||||||
Carousel = new BeatmapCarousel
|
Carousel = new TestBeatmapCarousel
|
||||||
{
|
{
|
||||||
|
NewItemsPresented = () => NewItemsPresentedInvocationCount++,
|
||||||
BleedTop = 50,
|
BleedTop = 50,
|
||||||
BleedBottom = 50,
|
BleedBottom = 50,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
@@ -127,12 +135,35 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Prefer title sorting so that order of carousel panels match order of BeatmapSets bindable.
|
||||||
|
SortBy(SortMode.Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void SortBy(FilterCriteria criteria) => AddStep($"sort:{criteria.Sort} group:{criteria.Group}", () => Carousel.Filter(criteria));
|
protected void SortBy(SortMode mode) => ApplyToFilter($"sort by {mode.GetDescription().ToLowerInvariant()}", c => c.Sort = mode);
|
||||||
|
protected void GroupBy(GroupMode mode) => ApplyToFilter($"group by {mode.GetDescription().ToLowerInvariant()}", c => c.Group = mode);
|
||||||
|
|
||||||
|
protected void SortAndGroupBy(SortMode sort, GroupMode group)
|
||||||
|
{
|
||||||
|
ApplyToFilter($"sort by {sort.GetDescription().ToLowerInvariant()} & group by {group.GetDescription().ToLowerInvariant()}", c =>
|
||||||
|
{
|
||||||
|
c.Sort = sort;
|
||||||
|
c.Group = group;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ApplyToFilter(string description, Action<FilterCriteria>? apply)
|
||||||
|
{
|
||||||
|
AddStep(description, () =>
|
||||||
|
{
|
||||||
|
var criteria = Carousel.Criteria;
|
||||||
|
apply?.Invoke(criteria);
|
||||||
|
Carousel.Filter(criteria);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType<ICarouselPanel>().Count(), () => Is.GreaterThan(0));
|
protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType<ICarouselPanel>().Count(), () => Is.GreaterThan(0));
|
||||||
protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False);
|
protected void WaitForFiltering() => AddUntilStep("filtering finished", () => Carousel.IsFiltering, () => Is.False);
|
||||||
protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target));
|
protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target));
|
||||||
|
|
||||||
protected void SelectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down));
|
protected void SelectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down));
|
||||||
@@ -145,6 +176,32 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
|
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
|
||||||
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
|
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
|
||||||
|
|
||||||
|
protected void CheckDisplayedBeatmapsCount(int expected)
|
||||||
|
{
|
||||||
|
AddAssert($"{expected} diffs displayed", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void CheckDisplayedBeatmapSetsCount(int expected)
|
||||||
|
{
|
||||||
|
AddAssert($"{expected} sets displayed", () =>
|
||||||
|
{
|
||||||
|
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
|
||||||
|
|
||||||
|
// Using groupingFilter.SetItems.Count alone doesn't work.
|
||||||
|
// When sorting by difficulty, there can be more than one set panel for the same set displayed.
|
||||||
|
return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is BeatmapSetInfo));
|
||||||
|
}, () => Is.EqualTo(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void CheckDisplayedGroupsCount(int expected)
|
||||||
|
{
|
||||||
|
AddAssert($"{expected} groups displayed", () =>
|
||||||
|
{
|
||||||
|
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
|
||||||
|
return groupingFilter.GroupItems.Count;
|
||||||
|
}, () => Is.EqualTo(expected));
|
||||||
|
}
|
||||||
|
|
||||||
protected ICarouselPanel? GetSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.Selected.Value);
|
protected ICarouselPanel? GetSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.Selected.Value);
|
||||||
protected ICarouselPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
|
protected ICarouselPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
|
||||||
|
|
||||||
@@ -296,5 +353,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class TestBeatmapCarousel : BeatmapCarousel
|
||||||
|
{
|
||||||
|
public IEnumerable<BeatmapInfo> PostFilterBeatmaps = null!;
|
||||||
|
|
||||||
|
protected override Task<IEnumerable<CarouselItem>> FilterAsync()
|
||||||
|
{
|
||||||
|
var filterAsync = base.FilterAsync();
|
||||||
|
filterAsync.ContinueWith(result =>
|
||||||
|
{
|
||||||
|
if (result.IsCompletedSuccessfully)
|
||||||
|
PostFilterBeatmaps = result.GetResultSafely().Select(i => i.Model).OfType<BeatmapInfo>();
|
||||||
|
});
|
||||||
|
return filterAsync;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
204
osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs
Normal file
204
osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Screens;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Online.Leaderboards;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Toolbar;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens;
|
||||||
|
using osu.Game.Screens.Footer;
|
||||||
|
using osu.Game.Screens.Menu;
|
||||||
|
using osu.Game.Screens.Select.Filter;
|
||||||
|
using osu.Game.Screens.SelectV2;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||||
|
{
|
||||||
|
public abstract partial class SongSelectTestScene : ScreenTestScene
|
||||||
|
{
|
||||||
|
protected BeatmapManager Beatmaps { get; private set; } = null!;
|
||||||
|
protected RealmRulesetStore Rulesets { get; private set; } = null!;
|
||||||
|
protected OsuConfigManager Config { get; private set; } = null!;
|
||||||
|
protected ScoreManager ScoreManager { get; private set; } = null!;
|
||||||
|
|
||||||
|
private RealmDetachedBeatmapStore beatmapStore = null!;
|
||||||
|
|
||||||
|
protected Screens.SelectV2.SongSelect SongSelect { get; private set; } = null!;
|
||||||
|
protected BeatmapCarousel Carousel => SongSelect.ChildrenOfType<BeatmapCarousel>().Single();
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
protected readonly ScreenFooter Footer;
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private readonly OsuLogo logo;
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private readonly VolumeOverlay volume;
|
||||||
|
|
||||||
|
[Cached(typeof(INotificationOverlay))]
|
||||||
|
private readonly INotificationOverlay notificationOverlay = new NotificationOverlay();
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
protected readonly LeaderboardManager LeaderboardManager = new LeaderboardManager();
|
||||||
|
|
||||||
|
protected SongSelectTestScene()
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new PopoverContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
LeaderboardManager,
|
||||||
|
new Toolbar
|
||||||
|
{
|
||||||
|
State = { Value = Visibility.Visible },
|
||||||
|
},
|
||||||
|
Footer = new ScreenFooter
|
||||||
|
{
|
||||||
|
BackButtonPressed = () => Stack.CurrentScreen.Exit(),
|
||||||
|
},
|
||||||
|
logo = new OsuLogo
|
||||||
|
{
|
||||||
|
Alpha = 0f,
|
||||||
|
},
|
||||||
|
volume = new VolumeOverlay(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Stack.Padding = new MarginPadding { Top = Toolbar.HEIGHT };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||||
|
{
|
||||||
|
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||||
|
|
||||||
|
// These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install.
|
||||||
|
// At a point we have isolated interactive test runs enough, this can likely be removed.
|
||||||
|
dependencies.Cache(Rulesets = new RealmRulesetStore(Realm));
|
||||||
|
dependencies.Cache(Realm);
|
||||||
|
dependencies.Cache(Beatmaps = new BeatmapManager(LocalStorage, Realm, null, Dependencies.Get<AudioManager>(), Resources, Dependencies.Get<GameHost>(), Beatmap.Default));
|
||||||
|
dependencies.Cache(Config = new OsuConfigManager(LocalStorage));
|
||||||
|
dependencies.Cache(ScoreManager = new ScoreManager(Rulesets, () => Beatmaps, LocalStorage, Realm, API, Config));
|
||||||
|
|
||||||
|
dependencies.CacheAs<BeatmapStore>(beatmapStore = new RealmDetachedBeatmapStore());
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Add(beatmapStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
Stack.ScreenPushed += updateFooter;
|
||||||
|
Stack.ScreenExited += updateFooter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetUpSteps()
|
||||||
|
{
|
||||||
|
base.SetUpSteps();
|
||||||
|
|
||||||
|
AddStep("reset defaults", () =>
|
||||||
|
{
|
||||||
|
Ruleset.Value = Rulesets.AvailableRulesets.First();
|
||||||
|
|
||||||
|
Beatmap.SetDefault();
|
||||||
|
SelectedMods.SetDefault();
|
||||||
|
|
||||||
|
Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title);
|
||||||
|
Config.SetValue(OsuSetting.SongSelectGroupingMode, GroupMode.All);
|
||||||
|
|
||||||
|
SongSelect = null!;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("delete all beatmaps", () => Beatmaps.Delete());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void LoadSongSelect()
|
||||||
|
{
|
||||||
|
AddStep("load screen", () => Stack.Push(SongSelect = new SoloSongSelect()));
|
||||||
|
AddUntilStep("wait for load", () => Stack.CurrentScreen == SongSelect && SongSelect.IsLoaded);
|
||||||
|
AddUntilStep("wait for filtering", () => !Carousel.IsFiltering);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ImportBeatmapForRuleset(int rulesetId)
|
||||||
|
{
|
||||||
|
int beatmapsCount = 0;
|
||||||
|
|
||||||
|
AddStep($"import test map for ruleset {rulesetId}", () =>
|
||||||
|
{
|
||||||
|
beatmapsCount = SongSelect.IsNull() ? 0 : Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single().SetItems.Count;
|
||||||
|
Beatmaps.Import(TestResources.CreateTestBeatmapSetInfo(3, Rulesets.AvailableRulesets.Where(r => r.OnlineID == rulesetId).ToArray()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is specifically for cases where the add is happening post song select load.
|
||||||
|
// For cases where song select is null, the assertions are provided by the load checks.
|
||||||
|
AddUntilStep("wait for imported to arrive in carousel", () => SongSelect.IsNull() || Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single().SetItems.Count > beatmapsCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ChangeMods(params Mod[] mods) => AddStep($"change mods to {string.Join(", ", mods.Select(m => m.Acronym))}", () => SelectedMods.Value = mods);
|
||||||
|
|
||||||
|
protected void ChangeRuleset(int rulesetId)
|
||||||
|
{
|
||||||
|
AddStep($"change ruleset to {rulesetId}", () => Ruleset.Value = Rulesets.AvailableRulesets.First(r => r.OnlineID == rulesetId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Imports test beatmap sets to show in the carousel.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="difficultyCountPerSet">
|
||||||
|
/// The exact count of difficulties to create for each beatmap set.
|
||||||
|
/// A <see langword="null"/> value causes the count of difficulties to be selected randomly.
|
||||||
|
/// </param>
|
||||||
|
protected void AddManyTestMaps(int? difficultyCountPerSet = null)
|
||||||
|
{
|
||||||
|
AddStep("import test maps", () =>
|
||||||
|
{
|
||||||
|
var usableRulesets = Rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray();
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
Beatmaps.Import(TestResources.CreateTestBeatmapSetInfo(difficultyCountPerSet, usableRulesets));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void WaitForSuspension() => AddUntilStep("wait for not current", () => !SongSelect.AsNonNull().IsCurrentScreen());
|
||||||
|
|
||||||
|
private void updateFooter(IScreen? _, IScreen? newScreen)
|
||||||
|
{
|
||||||
|
if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter)
|
||||||
|
{
|
||||||
|
Footer.Show();
|
||||||
|
Footer.SetButtons(osuScreen.CreateFooterButtons());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Footer.Hide();
|
||||||
|
Footer.SetButtons(Array.Empty<ScreenFooterButton>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ using System.Threading.Tasks;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Screens.Select;
|
|
||||||
using osu.Game.Screens.Select.Filter;
|
using osu.Game.Screens.Select.Filter;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
@@ -34,9 +33,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
[Explicit]
|
[Explicit]
|
||||||
public void TestSorting()
|
public void TestSorting()
|
||||||
{
|
{
|
||||||
SortBy(new FilterCriteria { Sort = SortMode.Artist });
|
SortAndGroupBy(SortMode.Artist, GroupMode.All);
|
||||||
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
|
SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty);
|
||||||
SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist });
|
SortAndGroupBy(SortMode.Artist, GroupMode.Artist);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ using System.Linq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Graphics.Carousel;
|
||||||
using osu.Game.Screens.Select.Filter;
|
using osu.Game.Screens.Select.Filter;
|
||||||
using osu.Game.Screens.SelectV2;
|
using osu.Game.Screens.SelectV2;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||||
{
|
{
|
||||||
@@ -19,7 +20,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
{
|
{
|
||||||
RemoveAllBeatmaps();
|
RemoveAllBeatmaps();
|
||||||
CreateCarousel();
|
CreateCarousel();
|
||||||
SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist });
|
|
||||||
|
SortAndGroupBy(SortMode.Artist, GroupMode.Artist);
|
||||||
|
|
||||||
AddBeatmaps(10, 3, true);
|
AddBeatmaps(10, 3, true);
|
||||||
WaitForDrawablePanels();
|
WaitForDrawablePanels();
|
||||||
@@ -173,5 +175,71 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
SelectNextGroup();
|
SelectNextGroup();
|
||||||
WaitForGroupSelection(1, 1);
|
WaitForGroupSelection(1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInputHandlingWithinGaps()
|
||||||
|
{
|
||||||
|
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
||||||
|
|
||||||
|
// Clicks just above the first group panel should not actuate any action.
|
||||||
|
ClickVisiblePanelWithOffset<PanelGroup>(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 + 1)));
|
||||||
|
|
||||||
|
AddAssert("no sets visible", () => !GetVisiblePanels<PanelBeatmapSet>().Any());
|
||||||
|
|
||||||
|
// add lenience to avoid floating-point inaccuracies at edge.
|
||||||
|
ClickVisiblePanelWithOffset<PanelGroup>(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 - 1)));
|
||||||
|
|
||||||
|
AddUntilStep("wait for sets visible", () => GetVisiblePanels<PanelBeatmapSet>().Any());
|
||||||
|
CheckNoSelection();
|
||||||
|
|
||||||
|
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
||||||
|
|
||||||
|
ClickVisiblePanelWithOffset<PanelBeatmapSet>(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||||
|
WaitForGroupSelection(0, 1);
|
||||||
|
|
||||||
|
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<PanelBeatmap>().Any());
|
||||||
|
|
||||||
|
// Beatmap panels expand their selection area to cover holes from spacing.
|
||||||
|
ClickVisiblePanelWithOffset<PanelBeatmap>(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||||
|
WaitForGroupSelection(0, 1);
|
||||||
|
|
||||||
|
ClickVisiblePanelWithOffset<PanelBeatmap>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||||
|
WaitForGroupSelection(0, 2);
|
||||||
|
|
||||||
|
ClickVisiblePanelWithOffset<PanelBeatmapSet>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||||
|
WaitForGroupSelection(0, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasicFiltering()
|
||||||
|
{
|
||||||
|
ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
CheckDisplayedGroupsCount(1);
|
||||||
|
CheckDisplayedBeatmapSetsCount(1);
|
||||||
|
CheckDisplayedBeatmapsCount(3);
|
||||||
|
|
||||||
|
CheckNoSelection();
|
||||||
|
SelectNextPanel();
|
||||||
|
Select();
|
||||||
|
SelectNextPanel();
|
||||||
|
Select();
|
||||||
|
WaitForGroupSelection(0, 1);
|
||||||
|
|
||||||
|
for (int i = 0; i < 6; i++)
|
||||||
|
SelectNextPanel();
|
||||||
|
|
||||||
|
Select();
|
||||||
|
|
||||||
|
WaitForGroupSelection(0, 2);
|
||||||
|
|
||||||
|
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
CheckDisplayedGroupsCount(5);
|
||||||
|
CheckDisplayedBeatmapSetsCount(10);
|
||||||
|
CheckDisplayedBeatmapsCount(30);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.Carousel;
|
using osu.Game.Graphics.Carousel;
|
||||||
using osu.Game.Screens.Select;
|
|
||||||
using osu.Game.Screens.Select.Filter;
|
using osu.Game.Screens.Select.Filter;
|
||||||
using osu.Game.Screens.SelectV2;
|
using osu.Game.Screens.SelectV2;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@@ -21,7 +20,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
{
|
{
|
||||||
RemoveAllBeatmaps();
|
RemoveAllBeatmaps();
|
||||||
CreateCarousel();
|
CreateCarousel();
|
||||||
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
|
|
||||||
|
SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty);
|
||||||
|
|
||||||
AddBeatmaps(10, 3);
|
AddBeatmaps(10, 3);
|
||||||
WaitForDrawablePanels();
|
WaitForDrawablePanels();
|
||||||
@@ -30,32 +30,32 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestOpenCloseGroupWithNoSelectionMouse()
|
public void TestOpenCloseGroupWithNoSelectionMouse()
|
||||||
{
|
{
|
||||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmapStandalone>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||||
CheckNoSelection();
|
CheckNoSelection();
|
||||||
|
|
||||||
ClickVisiblePanel<PanelGroup>(0);
|
ClickVisiblePanel<PanelGroupStarDifficulty>(0);
|
||||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmapStandalone>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||||
CheckNoSelection();
|
CheckNoSelection();
|
||||||
|
|
||||||
ClickVisiblePanel<PanelGroup>(0);
|
ClickVisiblePanel<PanelGroupStarDifficulty>(0);
|
||||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmapStandalone>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||||
CheckNoSelection();
|
CheckNoSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestOpenCloseGroupWithNoSelectionKeyboard()
|
public void TestOpenCloseGroupWithNoSelectionKeyboard()
|
||||||
{
|
{
|
||||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmapStandalone>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||||
CheckNoSelection();
|
CheckNoSelection();
|
||||||
|
|
||||||
SelectNextPanel();
|
SelectNextPanel();
|
||||||
Select();
|
Select();
|
||||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmapStandalone>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||||
AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||||
CheckNoSelection();
|
CheckNoSelection();
|
||||||
|
|
||||||
Select();
|
Select();
|
||||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmapStandalone>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||||
AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||||
CheckNoSelection();
|
CheckNoSelection();
|
||||||
}
|
}
|
||||||
@@ -88,10 +88,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||||
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||||
|
|
||||||
ClickVisiblePanel<PanelGroup>(0);
|
ClickVisiblePanel<PanelGroupStarDifficulty>(0);
|
||||||
AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null);
|
AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null);
|
||||||
|
|
||||||
ClickVisiblePanel<PanelGroup>(0);
|
ClickVisiblePanel<PanelGroupStarDifficulty>(0);
|
||||||
AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,18 +121,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
SelectNextGroup();
|
SelectNextGroup();
|
||||||
WaitForGroupSelection(0, 0);
|
WaitForGroupSelection(0, 0);
|
||||||
|
|
||||||
AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf<PanelBeatmap>);
|
AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf<PanelBeatmapStandalone>);
|
||||||
AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf<PanelBeatmap>);
|
AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf<PanelBeatmapStandalone>);
|
||||||
|
|
||||||
ClickVisiblePanel<PanelGroup>(0);
|
ClickVisiblePanel<PanelGroupStarDifficulty>(0);
|
||||||
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<PanelGroup>);
|
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<PanelGroupStarDifficulty>);
|
||||||
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||||
|
|
||||||
ClickVisiblePanel<PanelGroup>(0);
|
ClickVisiblePanel<PanelGroupStarDifficulty>(0);
|
||||||
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<PanelGroup>);
|
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<PanelGroupStarDifficulty>);
|
||||||
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||||
|
|
||||||
AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf<PanelBeatmap>);
|
AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf<PanelBeatmapStandalone>);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -147,7 +147,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
// open first group
|
// open first group
|
||||||
Select();
|
Select();
|
||||||
CheckNoSelection();
|
CheckNoSelection();
|
||||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmapStandalone>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||||
|
|
||||||
SelectNextPanel();
|
SelectNextPanel();
|
||||||
Select();
|
Select();
|
||||||
@@ -172,24 +172,57 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestInputHandlingWithinGaps()
|
public void TestInputHandlingWithinGaps()
|
||||||
{
|
{
|
||||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmapStandalone>().Any());
|
||||||
|
|
||||||
// Clicks just above the first group panel should not actuate any action.
|
// Clicks just above the first group panel should not actuate any action.
|
||||||
ClickVisiblePanelWithOffset<PanelGroup>(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 + 1)));
|
ClickVisiblePanelWithOffset<PanelGroupStarDifficulty>(0, new Vector2(0, -(PanelGroupStarDifficulty.HEIGHT / 2 + 1)));
|
||||||
|
|
||||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmapStandalone>().Any());
|
||||||
|
|
||||||
ClickVisiblePanelWithOffset<PanelGroup>(0, new Vector2(0, -(PanelGroup.HEIGHT / 2)));
|
// add lenience to avoid floating-point inaccuracies at edge.
|
||||||
|
ClickVisiblePanelWithOffset<PanelGroupStarDifficulty>(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 - 1)));
|
||||||
|
|
||||||
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<PanelBeatmap>().Any());
|
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<PanelBeatmapStandalone>().Any());
|
||||||
CheckNoSelection();
|
CheckNoSelection();
|
||||||
|
|
||||||
// Beatmap panels expand their selection area to cover holes from spacing.
|
// Beatmap panels expand their selection area to cover holes from spacing.
|
||||||
ClickVisiblePanelWithOffset<PanelBeatmap>(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
ClickVisiblePanelWithOffset<PanelBeatmapStandalone>(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||||
WaitForGroupSelection(0, 0);
|
WaitForGroupSelection(0, 0);
|
||||||
|
|
||||||
ClickVisiblePanelWithOffset<PanelBeatmap>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
ClickVisiblePanelWithOffset<PanelBeatmapStandalone>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||||
WaitForGroupSelection(0, 1);
|
WaitForGroupSelection(0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasicFiltering()
|
||||||
|
{
|
||||||
|
ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
CheckDisplayedGroupsCount(3);
|
||||||
|
CheckDisplayedBeatmapsCount(3);
|
||||||
|
|
||||||
|
CheckNoSelection();
|
||||||
|
SelectNextPanel();
|
||||||
|
Select();
|
||||||
|
SelectNextPanel();
|
||||||
|
Select();
|
||||||
|
WaitForGroupSelection(0, 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
SelectNextPanel();
|
||||||
|
|
||||||
|
Select();
|
||||||
|
SelectNextPanel();
|
||||||
|
Select();
|
||||||
|
|
||||||
|
WaitForGroupSelection(1, 0);
|
||||||
|
|
||||||
|
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
CheckDisplayedGroupsCount(3);
|
||||||
|
CheckDisplayedBeatmapsCount(30);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,294 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Screens.Select.Filter;
|
||||||
|
using osu.Game.Screens.SelectV2;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class TestSceneBeatmapCarouselFiltering : BeatmapCarouselTestScene
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private RulesetStore rulesets { get; set; } = null!;
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
RemoveAllBeatmaps();
|
||||||
|
CreateCarousel();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasicFiltering()
|
||||||
|
{
|
||||||
|
AddBeatmaps(10, 3);
|
||||||
|
WaitForDrawablePanels();
|
||||||
|
|
||||||
|
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(2));
|
||||||
|
|
||||||
|
CheckDisplayedBeatmapSetsCount(1);
|
||||||
|
CheckDisplayedBeatmapsCount(3);
|
||||||
|
|
||||||
|
SelectNextPanel();
|
||||||
|
Select();
|
||||||
|
|
||||||
|
WaitForSelection(2, 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
SelectNextPanel();
|
||||||
|
|
||||||
|
Select();
|
||||||
|
WaitForSelection(2, 1);
|
||||||
|
|
||||||
|
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(3));
|
||||||
|
|
||||||
|
CheckDisplayedBeatmapSetsCount(10);
|
||||||
|
CheckDisplayedBeatmapsCount(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestFilteringByUserStarDifficulty()
|
||||||
|
{
|
||||||
|
AddStep("add mixed difficulty set", () =>
|
||||||
|
{
|
||||||
|
var set = TestResources.CreateTestBeatmapSetInfo(1);
|
||||||
|
set.Beatmaps.Clear();
|
||||||
|
|
||||||
|
for (int i = 1; i <= 15; i++)
|
||||||
|
{
|
||||||
|
set.Beatmaps.Add(new BeatmapInfo(new OsuRuleset().RulesetInfo, new BeatmapDifficulty(), new BeatmapMetadata())
|
||||||
|
{
|
||||||
|
BeatmapSet = set,
|
||||||
|
DifficultyName = $"Stars: {i}",
|
||||||
|
StarRating = i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
BeatmapSets.Add(set);
|
||||||
|
});
|
||||||
|
|
||||||
|
WaitForDrawablePanels();
|
||||||
|
|
||||||
|
ApplyToFilter("filter [5..]", c =>
|
||||||
|
{
|
||||||
|
c.UserStarDifficulty.Min = 5;
|
||||||
|
c.UserStarDifficulty.Max = null;
|
||||||
|
});
|
||||||
|
WaitForFiltering();
|
||||||
|
CheckDisplayedBeatmapsCount(11);
|
||||||
|
|
||||||
|
ApplyToFilter("filter to [0..7]", c =>
|
||||||
|
{
|
||||||
|
c.UserStarDifficulty.Min = null;
|
||||||
|
c.UserStarDifficulty.Max = 7;
|
||||||
|
});
|
||||||
|
WaitForFiltering();
|
||||||
|
CheckDisplayedBeatmapsCount(7);
|
||||||
|
|
||||||
|
ApplyToFilter("filter to [5..7]", c =>
|
||||||
|
{
|
||||||
|
c.UserStarDifficulty.Min = 5;
|
||||||
|
c.UserStarDifficulty.Max = 7;
|
||||||
|
});
|
||||||
|
|
||||||
|
WaitForFiltering();
|
||||||
|
CheckDisplayedBeatmapsCount(3);
|
||||||
|
|
||||||
|
ApplyToFilter("filter to [2..2]", c =>
|
||||||
|
{
|
||||||
|
c.UserStarDifficulty.Min = 2;
|
||||||
|
c.UserStarDifficulty.Max = 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
WaitForFiltering();
|
||||||
|
CheckDisplayedBeatmapsCount(1);
|
||||||
|
|
||||||
|
ApplyToFilter("filter to [0..]", c =>
|
||||||
|
{
|
||||||
|
c.UserStarDifficulty.Min = 0;
|
||||||
|
c.UserStarDifficulty.Max = null;
|
||||||
|
});
|
||||||
|
WaitForFiltering();
|
||||||
|
CheckDisplayedBeatmapsCount(15);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCarouselRemembersSelection()
|
||||||
|
{
|
||||||
|
Guid selectedID = Guid.Empty;
|
||||||
|
|
||||||
|
AddBeatmaps(50, 3);
|
||||||
|
WaitForDrawablePanels();
|
||||||
|
|
||||||
|
SelectNextGroup();
|
||||||
|
SelectNextPanel();
|
||||||
|
Select();
|
||||||
|
|
||||||
|
AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID);
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
ApplyToFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString());
|
||||||
|
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||||
|
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||||
|
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCarouselRemembersSelectionDifficultySort()
|
||||||
|
{
|
||||||
|
Guid selectedID = Guid.Empty;
|
||||||
|
|
||||||
|
AddBeatmaps(50, 3);
|
||||||
|
WaitForDrawablePanels();
|
||||||
|
|
||||||
|
SortBy(SortMode.Difficulty);
|
||||||
|
|
||||||
|
SelectNextGroup();
|
||||||
|
|
||||||
|
AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID);
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
ApplyToFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString());
|
||||||
|
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||||
|
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||||
|
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCarouselRetainsSelectionFromDifficultySort()
|
||||||
|
{
|
||||||
|
AddBeatmaps(50, 3);
|
||||||
|
WaitForDrawablePanels();
|
||||||
|
|
||||||
|
BeatmapInfo chosenBeatmap = null!;
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
int diff = i;
|
||||||
|
|
||||||
|
AddStep($"select diff {diff}", () => Carousel.CurrentSelection = chosenBeatmap = BeatmapSets[20].Beatmaps[diff]);
|
||||||
|
AddUntilStep("selection changed", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
|
||||||
|
|
||||||
|
SortBy(SortMode.Difficulty);
|
||||||
|
AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
|
||||||
|
|
||||||
|
SortBy(SortMode.Title);
|
||||||
|
AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestExternalRulesetChange()
|
||||||
|
{
|
||||||
|
ApplyToFilter("allow converted beatmaps", c => c.AllowConvertedBeatmaps = true);
|
||||||
|
ApplyToFilter("filter to osu", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(0));
|
||||||
|
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddStep("add mixed ruleset beatmapset", () =>
|
||||||
|
{
|
||||||
|
var testMixed = TestResources.CreateTestBeatmapSetInfo(3);
|
||||||
|
|
||||||
|
for (int i = 0; i <= 2; i++)
|
||||||
|
testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i);
|
||||||
|
|
||||||
|
BeatmapSets.Add(testMixed);
|
||||||
|
});
|
||||||
|
WaitForDrawablePanels();
|
||||||
|
|
||||||
|
SelectNextPanel();
|
||||||
|
Select();
|
||||||
|
|
||||||
|
AddUntilStep("wait for filtered difficulties", () =>
|
||||||
|
{
|
||||||
|
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
|
||||||
|
|
||||||
|
return visibleBeatmapPanels.Count() == 1
|
||||||
|
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
ApplyToFilter("filter to taiko", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(1));
|
||||||
|
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddUntilStep("wait for filtered difficulties", () =>
|
||||||
|
{
|
||||||
|
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
|
||||||
|
|
||||||
|
return visibleBeatmapPanels.Count() == 2
|
||||||
|
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1
|
||||||
|
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 1) == 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
ApplyToFilter("filter to catch", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(2));
|
||||||
|
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddUntilStep("wait for filtered difficulties", () =>
|
||||||
|
{
|
||||||
|
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
|
||||||
|
|
||||||
|
return visibleBeatmapPanels.Count() == 2
|
||||||
|
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1
|
||||||
|
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 2) == 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSortingWithDifficultyFiltered()
|
||||||
|
{
|
||||||
|
const int diffs_per_set = 3;
|
||||||
|
const int local_set_count = 2;
|
||||||
|
|
||||||
|
AddStep("populate beatmap sets", () =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < local_set_count; i++)
|
||||||
|
{
|
||||||
|
var set = TestResources.CreateTestBeatmapSetInfo(diffs_per_set);
|
||||||
|
set.Beatmaps[0].StarRating = 3 - i;
|
||||||
|
set.Beatmaps[0].DifficultyName += $" ({3 - i}*)";
|
||||||
|
set.Beatmaps[1].StarRating = 6 + i;
|
||||||
|
set.Beatmaps[1].DifficultyName += $" ({6 + i}*)";
|
||||||
|
BeatmapSets.Add(set);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SortBy(SortMode.Difficulty);
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
CheckDisplayedBeatmapsCount(local_set_count * diffs_per_set);
|
||||||
|
|
||||||
|
ApplyToFilter("filter to normal", c => c.SearchText = "Normal");
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
CheckDisplayedBeatmapsCount(local_set_count);
|
||||||
|
|
||||||
|
ApplyToFilter("filter to insane", c => c.SearchText = "Insane");
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
CheckDisplayedBeatmapsCount(local_set_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.Carousel;
|
using osu.Game.Graphics.Carousel;
|
||||||
using osu.Game.Screens.Select;
|
|
||||||
using osu.Game.Screens.Select.Filter;
|
using osu.Game.Screens.Select.Filter;
|
||||||
using osu.Game.Screens.SelectV2;
|
using osu.Game.Screens.SelectV2;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@@ -22,7 +21,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
{
|
{
|
||||||
RemoveAllBeatmaps();
|
RemoveAllBeatmaps();
|
||||||
CreateCarousel();
|
CreateCarousel();
|
||||||
SortBy(new FilterCriteria { Sort = SortMode.Title });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -221,7 +219,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
|
|
||||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
||||||
|
|
||||||
ClickVisiblePanelWithOffset<PanelBeatmapSet>(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2)));
|
// add lenience to avoid floating-point inaccuracies at edge.
|
||||||
|
ClickVisiblePanelWithOffset<PanelBeatmapSet>(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2 - 1)));
|
||||||
|
|
||||||
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<PanelBeatmap>().Any());
|
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<PanelBeatmap>().Any());
|
||||||
WaitForSelection(0, 0);
|
WaitForSelection(0, 0);
|
||||||
@@ -238,6 +237,30 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
WaitForSelection(0, 3);
|
WaitForSelection(0, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDifficultySortingWithNoGroups()
|
||||||
|
{
|
||||||
|
AddBeatmaps(2, 3);
|
||||||
|
WaitForDrawablePanels();
|
||||||
|
|
||||||
|
SortAndGroupBy(SortMode.Difficulty, GroupMode.All);
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddUntilStep("standalone panels displayed", () => GetVisiblePanels<PanelBeatmapStandalone>().Any());
|
||||||
|
|
||||||
|
SelectNextGroup();
|
||||||
|
// both sets have a difficulty with 0.00* star rating.
|
||||||
|
// in the case of a tie when sorting, the first tie-breaker is `DateAdded` descending, which will pick the last set added (see `TestResources.CreateTestBeatmapSetInfo()`).
|
||||||
|
WaitForSelection(1, 0);
|
||||||
|
|
||||||
|
SelectNextGroup();
|
||||||
|
WaitForSelection(0, 0);
|
||||||
|
|
||||||
|
SelectNextPanel();
|
||||||
|
Select();
|
||||||
|
WaitForSelection(1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
private void checkSelectionIterating(bool isIterating)
|
private void checkSelectionIterating(bool isIterating)
|
||||||
{
|
{
|
||||||
object? selection = null;
|
object? selection = null;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using System.Linq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Screens.Select;
|
|
||||||
using osu.Game.Screens.SelectV2;
|
using osu.Game.Screens.SelectV2;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||||
@@ -18,37 +17,59 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
{
|
{
|
||||||
RemoveAllBeatmaps();
|
RemoveAllBeatmaps();
|
||||||
CreateCarousel();
|
CreateCarousel();
|
||||||
SortBy(new FilterCriteria());
|
|
||||||
|
|
||||||
AddBeatmaps(10);
|
AddBeatmaps(10);
|
||||||
WaitForDrawablePanels();
|
WaitForDrawablePanels();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestScrollPositionMaintainedOnAddSecondSelected()
|
public void TestScrollPositionMaintainedOnRemove_SecondSelected()
|
||||||
{
|
{
|
||||||
Quad positionBefore = default;
|
Quad positionBefore = default;
|
||||||
|
|
||||||
AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First());
|
AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First());
|
||||||
AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value)));
|
|
||||||
|
|
||||||
WaitForScrolling();
|
WaitForScrolling();
|
||||||
|
|
||||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||||
|
|
||||||
RemoveFirstBeatmap();
|
RemoveFirstBeatmap();
|
||||||
WaitForSorting();
|
WaitForFiltering();
|
||||||
|
|
||||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||||
() => Is.EqualTo(positionBefore));
|
() => Is.EqualTo(positionBefore));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestScrollPositionMaintainedOnAddLastSelected()
|
public void TestScrollPositionMaintainedOnRemove_SecondSelected_WithUserScroll()
|
||||||
{
|
{
|
||||||
Quad positionBefore = default;
|
Quad positionBefore = default;
|
||||||
|
|
||||||
AddStep("scroll to last item", () => Scroll.ScrollToEnd(false));
|
AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First());
|
||||||
|
WaitForScrolling();
|
||||||
|
|
||||||
|
AddStep("override scroll with user scroll", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(Scroll.ScreenSpaceDrawQuad.Centre);
|
||||||
|
InputManager.ScrollVerticalBy(-1);
|
||||||
|
});
|
||||||
|
WaitForScrolling();
|
||||||
|
|
||||||
|
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||||
|
|
||||||
|
RemoveFirstBeatmap();
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||||
|
() => Is.EqualTo(positionBefore));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScrollPositionMaintainedOnRemove_LastSelected()
|
||||||
|
{
|
||||||
|
Quad positionBefore = default;
|
||||||
|
|
||||||
|
AddStep("scroll to end", () => Scroll.ScrollToEnd(false));
|
||||||
|
|
||||||
AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last().Beatmaps.Last());
|
AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last().Beatmaps.Last());
|
||||||
|
|
||||||
@@ -57,9 +78,54 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||||
|
|
||||||
RemoveFirstBeatmap();
|
RemoveFirstBeatmap();
|
||||||
WaitForSorting();
|
WaitForFiltering();
|
||||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||||
() => Is.EqualTo(positionBefore));
|
() => Is.EqualTo(positionBefore));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScrollToSelectionAfterFilter()
|
||||||
|
{
|
||||||
|
Quad positionBefore = default;
|
||||||
|
|
||||||
|
AddStep("select first beatmap", () => Carousel.CurrentSelection = BeatmapSets.First().Beatmaps.First());
|
||||||
|
|
||||||
|
WaitForScrolling();
|
||||||
|
|
||||||
|
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||||
|
|
||||||
|
AddStep("scroll to end", () => Scroll.ScrollToEnd());
|
||||||
|
WaitForScrolling();
|
||||||
|
|
||||||
|
ApplyToFilter("search", f => f.SearchText = "Some");
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddUntilStep("select screen position returned to selection", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||||
|
() => Is.EqualTo(positionBefore));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScrollToSelectionAfterFilter_WithUserScroll()
|
||||||
|
{
|
||||||
|
Quad positionBefore = default;
|
||||||
|
|
||||||
|
AddStep("select first beatmap", () => Carousel.CurrentSelection = BeatmapSets.First().Beatmaps.First());
|
||||||
|
WaitForScrolling();
|
||||||
|
|
||||||
|
AddStep("override scroll with user scroll", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(Scroll.ScreenSpaceDrawQuad.Centre);
|
||||||
|
InputManager.ScrollVerticalBy(-1);
|
||||||
|
});
|
||||||
|
WaitForScrolling();
|
||||||
|
|
||||||
|
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||||
|
|
||||||
|
ApplyToFilter("search", f => f.SearchText = "Some");
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddUntilStep("select screen position returned to selection", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||||
|
() => Is.EqualTo(positionBefore));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Extensions;
|
using osu.Game.Extensions;
|
||||||
|
using osu.Game.Screens.Select.Filter;
|
||||||
using osu.Game.Screens.SelectV2;
|
using osu.Game.Screens.SelectV2;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
BeatmapSets.Add(baseTestBeatmap);
|
BeatmapSets.Add(baseTestBeatmap);
|
||||||
});
|
});
|
||||||
|
|
||||||
WaitForSorting();
|
WaitForFiltering();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -55,7 +56,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
|
|
||||||
AddStep("update beatmap with same reference", () => BeatmapSets.ReplaceRange(1, 1, [baseTestBeatmap]));
|
AddStep("update beatmap with same reference", () => BeatmapSets.ReplaceRange(1, 1, [baseTestBeatmap]));
|
||||||
|
|
||||||
WaitForSorting();
|
WaitForFiltering();
|
||||||
AddAssert("drawables unchanged", () => Carousel.ChildrenOfType<Panel>(), () => Is.EqualTo(originalDrawables));
|
AddAssert("drawables unchanged", () => Carousel.ChildrenOfType<Panel>(), () => Is.EqualTo(originalDrawables));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,21 +79,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
|
|
||||||
updateBeatmap(b => b.Metadata = metadata);
|
updateBeatmap(b => b.Metadata = metadata);
|
||||||
|
|
||||||
WaitForSorting();
|
WaitForFiltering();
|
||||||
AddAssert("drawables changed", () => Carousel.ChildrenOfType<Panel>(), () => Is.Not.EqualTo(originalDrawables));
|
AddAssert("drawables changed", () => Carousel.ChildrenOfType<Panel>(), () => Is.Not.EqualTo(originalDrawables));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSelectionHeld()
|
public void TestSelectionHeld()
|
||||||
{
|
{
|
||||||
SelectPrevGroup();
|
SelectNextGroup();
|
||||||
|
|
||||||
WaitForSelection(1, 0);
|
WaitForSelection(1, 0);
|
||||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||||
|
|
||||||
updateBeatmap();
|
updateBeatmap();
|
||||||
WaitForSorting();
|
WaitForFiltering();
|
||||||
|
|
||||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||||
@@ -101,14 +102,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
[Test] // Checks that we keep selection based on online ID where possible.
|
[Test] // Checks that we keep selection based on online ID where possible.
|
||||||
public void TestSelectionHeldDifficultyNameChanged()
|
public void TestSelectionHeldDifficultyNameChanged()
|
||||||
{
|
{
|
||||||
SelectPrevGroup();
|
SelectNextGroup();
|
||||||
|
|
||||||
WaitForSelection(1, 0);
|
WaitForSelection(1, 0);
|
||||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||||
|
|
||||||
updateBeatmap(b => b.DifficultyName = "new name");
|
updateBeatmap(b => b.DifficultyName = "new name");
|
||||||
WaitForSorting();
|
WaitForFiltering();
|
||||||
|
|
||||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||||
@@ -117,19 +118,137 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
[Test] // Checks that we fallback to keeping selection based on difficulty name.
|
[Test] // Checks that we fallback to keeping selection based on difficulty name.
|
||||||
public void TestSelectionHeldDifficultyOnlineIDChanged()
|
public void TestSelectionHeldDifficultyOnlineIDChanged()
|
||||||
{
|
{
|
||||||
SelectPrevGroup();
|
SelectNextGroup();
|
||||||
|
|
||||||
WaitForSelection(1, 0);
|
WaitForSelection(1, 0);
|
||||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||||
|
|
||||||
updateBeatmap(b => b.OnlineID = b.OnlineID + 1);
|
updateBeatmap(b => b.OnlineID = b.OnlineID + 1);
|
||||||
WaitForSorting();
|
WaitForFiltering();
|
||||||
|
|
||||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures stability is maintained on different sort modes while an item is removed and then immediately re-added.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestSortingStabilityWithRemovedAndReaddedItem()
|
||||||
|
{
|
||||||
|
RemoveAllBeatmaps();
|
||||||
|
|
||||||
|
const int diff_count = 5;
|
||||||
|
|
||||||
|
AddStep("Populate beatmap sets", () =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
|
|
||||||
|
// only need to set the first as they are a shared reference.
|
||||||
|
var beatmap = set.Beatmaps.First();
|
||||||
|
|
||||||
|
beatmap.Metadata.Artist = "same artist";
|
||||||
|
beatmap.Metadata.Title = "same title";
|
||||||
|
|
||||||
|
// testing the case where DateAdded happens to equal (quite rare).
|
||||||
|
set.DateAdded = DateTimeOffset.UnixEpoch;
|
||||||
|
|
||||||
|
BeatmapSets.Add(set);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
BeatmapSetInfo removedBeatmap = null!;
|
||||||
|
Guid[] originalOrder = null!;
|
||||||
|
|
||||||
|
SortBy(SortMode.Artist);
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddAssert("Items in descending added order", () => Carousel.PostFilterBeatmaps.Select(b => b.BeatmapSet!.DateAdded), () => Is.Ordered.Descending);
|
||||||
|
AddStep("Save order", () => originalOrder = Carousel.PostFilterBeatmaps.Select(b => b.ID).ToArray());
|
||||||
|
|
||||||
|
AddStep("Remove item", () =>
|
||||||
|
{
|
||||||
|
removedBeatmap = BeatmapSets[1];
|
||||||
|
BeatmapSets.RemoveAt(1);
|
||||||
|
});
|
||||||
|
AddStep("Re-add item", () => BeatmapSets.Insert(1, removedBeatmap));
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddAssert("Order didn't change", () => Carousel.PostFilterBeatmaps.Select(b => b.ID), () => Is.EqualTo(originalOrder));
|
||||||
|
|
||||||
|
SortBy(SortMode.Title);
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddAssert("Order didn't change", () => Carousel.PostFilterBeatmaps.Select(b => b.ID), () => Is.EqualTo(originalOrder));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures stability is maintained on different sort modes while a new item is added to the carousel.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestSortingStabilityWithNewItems()
|
||||||
|
{
|
||||||
|
RemoveAllBeatmaps();
|
||||||
|
|
||||||
|
const int diff_count = 5;
|
||||||
|
|
||||||
|
AddStep("Populate beatmap sets", () =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
|
|
||||||
|
// only need to set the first as they are a shared reference.
|
||||||
|
var beatmap = set.Beatmaps.First();
|
||||||
|
|
||||||
|
beatmap.Metadata.Artist = "same artist";
|
||||||
|
beatmap.Metadata.Title = "same title";
|
||||||
|
|
||||||
|
// testing the case where DateAdded happens to equal (quite rare).
|
||||||
|
set.DateAdded = DateTimeOffset.UnixEpoch;
|
||||||
|
|
||||||
|
BeatmapSets.Add(set);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Guid[] originalOrder = null!;
|
||||||
|
|
||||||
|
SortBy(SortMode.Artist);
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddAssert("Items in descending added order", () => Carousel.PostFilterBeatmaps.Select(b => b.BeatmapSet!.DateAdded), () => Is.Ordered.Descending);
|
||||||
|
AddStep("Save order", () => originalOrder = Carousel.PostFilterBeatmaps.Select(b => b.ID).ToArray());
|
||||||
|
|
||||||
|
AddStep("Add new item", () =>
|
||||||
|
{
|
||||||
|
var set = TestResources.CreateTestBeatmapSetInfo();
|
||||||
|
|
||||||
|
// only need to set the first as they are a shared reference.
|
||||||
|
var beatmap = set.Beatmaps.First();
|
||||||
|
|
||||||
|
beatmap.Metadata.Artist = "same artist";
|
||||||
|
beatmap.Metadata.Title = "same title";
|
||||||
|
|
||||||
|
set.DateAdded = DateTimeOffset.FromUnixTimeSeconds(1);
|
||||||
|
|
||||||
|
BeatmapSets.Add(set);
|
||||||
|
|
||||||
|
// add set to expected ordering
|
||||||
|
originalOrder = set.Beatmaps.Select(b => b.ID).Concat(originalOrder).ToArray();
|
||||||
|
});
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddAssert("Order didn't change", () => Carousel.PostFilterBeatmaps.Select(b => b.ID), () => Is.EqualTo(originalOrder));
|
||||||
|
|
||||||
|
SortBy(SortMode.Title);
|
||||||
|
WaitForFiltering();
|
||||||
|
|
||||||
|
AddAssert("Order didn't change", () => Carousel.PostFilterBeatmaps.Select(b => b.ID), () => Is.EqualTo(originalOrder));
|
||||||
|
}
|
||||||
|
|
||||||
private void updateBeatmap(Action<BeatmapInfo>? updateBeatmap = null, Action<BeatmapSetInfo>? updateSet = null)
|
private void updateBeatmap(Action<BeatmapInfo>? updateBeatmap = null, Action<BeatmapSetInfo>? updateSet = null)
|
||||||
{
|
{
|
||||||
AddStep("update beatmap with different reference", () =>
|
AddStep("update beatmap with different reference", () =>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
{
|
{
|
||||||
public partial class TestSceneBeatmapFilterControl : SongSelectComponentsTestScene
|
public partial class TestSceneBeatmapFilterControl : SongSelectComponentsTestScene
|
||||||
{
|
{
|
||||||
|
private FilterControl filterControl = null!;
|
||||||
|
|
||||||
protected override Anchor ComponentAnchor => Anchor.TopRight;
|
protected override Anchor ComponentAnchor => Anchor.TopRight;
|
||||||
protected override float InitialRelativeWidth => 0.7f;
|
protected override float InitialRelativeWidth => 0.7f;
|
||||||
|
|
||||||
@@ -20,12 +22,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Child = new FilterControl
|
Child = filterControl = new FilterControl
|
||||||
{
|
{
|
||||||
State = { Value = Visibility.Visible },
|
State = { Value = Visibility.Visible },
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSearch()
|
||||||
|
{
|
||||||
|
AddStep("search for text", () => filterControl.Search("test search"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
{
|
{
|
||||||
public partial class TestSceneFooterButtonMods : OsuTestScene
|
public partial class TestSceneFooterButtonMods : OsuTestScene
|
||||||
{
|
{
|
||||||
private readonly TestScreenFooterButtonMods footerButtonMods;
|
private readonly FooterButtonMods footerButtonMods;
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||||
|
|
||||||
public TestSceneFooterButtonMods()
|
public TestSceneFooterButtonMods()
|
||||||
{
|
{
|
||||||
Add(footerButtonMods = new TestScreenFooterButtonMods(new TestModSelectOverlay())
|
Add(footerButtonMods = new FooterButtonMods(new TestModSelectOverlay())
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
@@ -63,19 +63,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
{
|
{
|
||||||
var hiddenMod = new Mod[] { new OsuModHidden() };
|
var hiddenMod = new Mod[] { new OsuModHidden() };
|
||||||
AddStep(@"Add Hidden", () => changeMods(hiddenMod));
|
AddStep(@"Add Hidden", () => changeMods(hiddenMod));
|
||||||
AddAssert(@"Check Hidden multiplier", () => assertModsMultiplier(hiddenMod));
|
assertModsMultiplier(hiddenMod);
|
||||||
|
|
||||||
var hardRockMod = new Mod[] { new OsuModHardRock() };
|
var hardRockMod = new Mod[] { new OsuModHardRock() };
|
||||||
AddStep(@"Add HardRock", () => changeMods(hardRockMod));
|
AddStep(@"Add HardRock", () => changeMods(hardRockMod));
|
||||||
AddAssert(@"Check HardRock multiplier", () => assertModsMultiplier(hardRockMod));
|
assertModsMultiplier(hardRockMod);
|
||||||
|
|
||||||
var doubleTimeMod = new Mod[] { new OsuModDoubleTime() };
|
var doubleTimeMod = new Mod[] { new OsuModDoubleTime() };
|
||||||
AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod));
|
AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod));
|
||||||
AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod));
|
assertModsMultiplier(doubleTimeMod);
|
||||||
|
|
||||||
var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
|
var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
|
||||||
AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods));
|
AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods));
|
||||||
AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods));
|
assertModsMultiplier(multipleIncrementMods);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -83,15 +83,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
{
|
{
|
||||||
var easyMod = new Mod[] { new OsuModEasy() };
|
var easyMod = new Mod[] { new OsuModEasy() };
|
||||||
AddStep(@"Add Easy", () => changeMods(easyMod));
|
AddStep(@"Add Easy", () => changeMods(easyMod));
|
||||||
AddAssert(@"Check Easy multiplier", () => assertModsMultiplier(easyMod));
|
assertModsMultiplier(easyMod);
|
||||||
|
|
||||||
var noFailMod = new Mod[] { new OsuModNoFail() };
|
var noFailMod = new Mod[] { new OsuModNoFail() };
|
||||||
AddStep(@"Add NoFail", () => changeMods(noFailMod));
|
AddStep(@"Add NoFail", () => changeMods(noFailMod));
|
||||||
AddAssert(@"Check NoFail multiplier", () => assertModsMultiplier(noFailMod));
|
assertModsMultiplier(noFailMod);
|
||||||
|
|
||||||
var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() };
|
var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() };
|
||||||
AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods));
|
AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods));
|
||||||
AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleDecrementMods));
|
assertModsMultiplier(multipleDecrementMods);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -105,12 +105,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
|
|
||||||
private void changeMods(IReadOnlyList<Mod> mods) => footerButtonMods.Current.Value = mods;
|
private void changeMods(IReadOnlyList<Mod> mods) => footerButtonMods.Current.Value = mods;
|
||||||
|
|
||||||
private bool assertModsMultiplier(IEnumerable<Mod> mods)
|
private void assertModsMultiplier(IEnumerable<Mod> mods)
|
||||||
{
|
{
|
||||||
double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||||
string expectedValue = ModUtils.FormatScoreMultiplier(multiplier).ToString();
|
string expectedValue = ModUtils.FormatScoreMultiplier(multiplier).ToString();
|
||||||
|
|
||||||
return expectedValue == footerButtonMods.MultiplierText.Current.Value;
|
AddAssert($"Displayed multiplier is {expectedValue}", () => footerButtonMods.ChildrenOfType<OsuSpriteText>().First(t => t.Text.ToString().Contains('x')).Text.ToString(), () => Is.EqualTo(expectedValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
private partial class TestModSelectOverlay : UserModSelectOverlay
|
private partial class TestModSelectOverlay : UserModSelectOverlay
|
||||||
@@ -121,15 +121,5 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
ShowPresets = true;
|
ShowPresets = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private partial class TestScreenFooterButtonMods : FooterButtonMods
|
|
||||||
{
|
|
||||||
public new OsuSpriteText MultiplierText => base.MultiplierText;
|
|
||||||
|
|
||||||
public TestScreenFooterButtonMods(ModSelectOverlay overlay)
|
|
||||||
: base(overlay)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Graphics.Carousel;
|
using osu.Game.Graphics.Carousel;
|
||||||
using osu.Game.Screens.SelectV2;
|
using osu.Game.Screens.SelectV2;
|
||||||
@@ -55,21 +56,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
{
|
{
|
||||||
new PanelGroupStarDifficulty
|
new PanelGroupStarDifficulty
|
||||||
{
|
{
|
||||||
Item = new CarouselItem(new GroupDefinition(star, star.ToString()))
|
Item = new CarouselItem(new GroupDefinition(new StarDifficulty(star, 0), $"{star} Star(s)"))
|
||||||
},
|
},
|
||||||
new PanelGroupStarDifficulty
|
new PanelGroupStarDifficulty
|
||||||
{
|
{
|
||||||
Item = new CarouselItem(new GroupDefinition(star, star.ToString())),
|
Item = new CarouselItem(new GroupDefinition(new StarDifficulty(star, 0), $"{star} Star(s)")),
|
||||||
KeyboardSelected = { Value = true },
|
KeyboardSelected = { Value = true },
|
||||||
},
|
},
|
||||||
new PanelGroupStarDifficulty
|
new PanelGroupStarDifficulty
|
||||||
{
|
{
|
||||||
Item = new CarouselItem(new GroupDefinition(star, star.ToString())),
|
Item = new CarouselItem(new GroupDefinition(new StarDifficulty(star, 0), $"{star} Star(s)")),
|
||||||
Expanded = { Value = true },
|
Expanded = { Value = true },
|
||||||
},
|
},
|
||||||
new PanelGroupStarDifficulty
|
new PanelGroupStarDifficulty
|
||||||
{
|
{
|
||||||
Item = new CarouselItem(new GroupDefinition(star, star.ToString())),
|
Item = new CarouselItem(new GroupDefinition(new StarDifficulty(star, 0), $"{star} Star(s)")),
|
||||||
Expanded = { Value = true },
|
Expanded = { Value = true },
|
||||||
KeyboardSelected = { Value = true },
|
KeyboardSelected = { Value = true },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,97 +5,349 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Cursor;
|
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Database;
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.Leaderboards;
|
||||||
|
using osu.Game.Overlays.Dialog;
|
||||||
using osu.Game.Overlays.Mods;
|
using osu.Game.Overlays.Mods;
|
||||||
using osu.Game.Rulesets.Catch;
|
|
||||||
using osu.Game.Rulesets.Mania;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Taiko;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Footer;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Screens.Menu;
|
using osu.Game.Screens.Select;
|
||||||
|
using osu.Game.Screens.Select.Leaderboards;
|
||||||
using osu.Game.Screens.SelectV2;
|
using osu.Game.Screens.SelectV2;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
using FooterButtonMods = osu.Game.Screens.SelectV2.FooterButtonMods;
|
||||||
|
using FooterButtonOptions = osu.Game.Screens.SelectV2.FooterButtonOptions;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||||
{
|
{
|
||||||
public partial class TestSceneSongSelect : ScreenTestScene
|
public partial class TestSceneSongSelect : SongSelectTestScene
|
||||||
{
|
{
|
||||||
[Cached]
|
[Test]
|
||||||
private readonly ScreenFooter screenScreenFooter;
|
public void TestResultsScreenWhenClickingLeaderboardScore()
|
||||||
|
|
||||||
[Cached]
|
|
||||||
private readonly OsuLogo logo;
|
|
||||||
|
|
||||||
protected override bool UseOnlineAPI => true;
|
|
||||||
|
|
||||||
public TestSceneSongSelect()
|
|
||||||
{
|
{
|
||||||
Children = new Drawable[]
|
LoadSongSelect();
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
|
||||||
|
AddAssert("beatmap imported", () => Beatmaps.GetAllUsableBeatmapSets().Any(), () => Is.True);
|
||||||
|
|
||||||
|
// song select should automatically select the beatmap for us but this is not implemented yet.
|
||||||
|
// todo: remove when that's the case.
|
||||||
|
AddAssert("no beatmap selected", () => Beatmap.IsDefault);
|
||||||
|
AddStep("select beatmap", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmaps.GetAllUsableBeatmapSets().Single().Beatmaps.First()));
|
||||||
|
AddAssert("beatmap selected", () => !Beatmap.IsDefault);
|
||||||
|
|
||||||
|
AddStep("import score", () =>
|
||||||
{
|
{
|
||||||
new PopoverContainer
|
var beatmapInfo = Beatmaps.GetAllUsableBeatmapSets().Single().Beatmaps.First();
|
||||||
|
ScoreManager.Import(new ScoreInfo
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
Hash = Guid.NewGuid().ToString(),
|
||||||
Child = screenScreenFooter = new ScreenFooter
|
BeatmapHash = beatmapInfo.Hash,
|
||||||
{
|
BeatmapInfo = beatmapInfo,
|
||||||
OnBack = () => Stack.CurrentScreen.Exit(),
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
},
|
User = new GuestUser(),
|
||||||
},
|
});
|
||||||
logo = new OsuLogo
|
});
|
||||||
{
|
|
||||||
Alpha = 0f,
|
AddStep("select ranking tab", () =>
|
||||||
},
|
{
|
||||||
};
|
InputManager.MoveMouseTo(SongSelect.ChildrenOfType<BeatmapDetailsArea.WedgeSelector<BeatmapDetailsArea.Header.Selection>>().Last());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
// probably should be done via dropdown menu instead of forcing this way?
|
||||||
|
AddStep("set local scope", () =>
|
||||||
|
{
|
||||||
|
var current = LeaderboardManager.CurrentCriteria!;
|
||||||
|
LeaderboardManager.FetchWithCriteria(new LeaderboardCriteria(current.Beatmap, current.Ruleset, BeatmapLeaderboardScope.Local, null));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for score panel", () => SongSelect.ChildrenOfType<BeatmapLeaderboardScore>().Any());
|
||||||
|
AddStep("click score panel", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(SongSelect.ChildrenOfType<BeatmapLeaderboardScore>().Single());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddUntilStep("wait for results screen", () => Stack.CurrentScreen is ResultsScreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
#region Hotkeys
|
||||||
private void load()
|
|
||||||
|
[Test]
|
||||||
|
public void TestDeleteHotkey()
|
||||||
{
|
{
|
||||||
RealmDetachedBeatmapStore beatmapStore;
|
LoadSongSelect();
|
||||||
|
|
||||||
Dependencies.CacheAs<BeatmapStore>(beatmapStore = new RealmDetachedBeatmapStore());
|
ImportBeatmapForRuleset(0);
|
||||||
Add(beatmapStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
AddAssert("beatmap imported", () => Beatmaps.GetAllUsableBeatmapSets().Any(), () => Is.True);
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
Stack.ScreenPushed += updateFooter;
|
// song select should automatically select the beatmap for us but this is not implemented yet.
|
||||||
Stack.ScreenExited += updateFooter;
|
// todo: remove when that's the case.
|
||||||
}
|
AddAssert("no beatmap selected", () => Beatmap.IsDefault);
|
||||||
|
AddStep("select beatmap", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmaps.GetAllUsableBeatmapSets().Single().Beatmaps.First()));
|
||||||
|
AddAssert("beatmap selected", () => !Beatmap.IsDefault);
|
||||||
|
|
||||||
[SetUpSteps]
|
AddStep("press shift-delete", () =>
|
||||||
public override void SetUpSteps()
|
{
|
||||||
{
|
InputManager.PressKey(Key.ShiftLeft);
|
||||||
base.SetUpSteps();
|
InputManager.Key(Key.Delete);
|
||||||
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
|
});
|
||||||
|
|
||||||
AddStep("load screen", () => Stack.Push(new SoloSongSelect()));
|
AddUntilStep("delete dialog shown", () => DialogOverlay.CurrentDialog, Is.InstanceOf<BeatmapDeleteDialog>);
|
||||||
AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded);
|
AddStep("confirm deletion", () => DialogOverlay.CurrentDialog!.PerformAction<PopupDialogDangerousButton>());
|
||||||
|
|
||||||
|
AddAssert("beatmap set deleted", () => Beatmaps.GetAllUsableBeatmapSets().Any(), () => Is.False);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRulesets()
|
public void TestClearModsViaModButtonRightClick()
|
||||||
{
|
{
|
||||||
AddStep("set osu ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
|
LoadSongSelect();
|
||||||
AddStep("set taiko ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
|
|
||||||
AddStep("set catch ruleset", () => Ruleset.Value = new CatchRuleset().RulesetInfo);
|
AddStep("select NC", () => SelectedMods.Value = new[] { new OsuModNightcore() });
|
||||||
AddStep("set mania ruleset", () => Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
AddAssert("mods selected", () => SelectedMods.Value, () => Has.Count.EqualTo(1));
|
||||||
|
AddStep("right click mod button", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(Footer.ChildrenOfType<FooterButtonMods>().Single());
|
||||||
|
InputManager.Click(MouseButton.Right);
|
||||||
|
});
|
||||||
|
AddAssert("not mods selected", () => SelectedMods.Value, () => Has.Count.EqualTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSpeedChange()
|
||||||
|
{
|
||||||
|
LoadSongSelect();
|
||||||
|
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("half time activated at 0.95x", () => SelectedMods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("half time speed changed to 0.9x", () => SelectedMods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005));
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("half time speed changed to 0.95x", () => SelectedMods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("no mods selected", () => SelectedMods.Value.Count == 0);
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("double time activated at 1.05x", () => SelectedMods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("double time speed changed to 1.1x", () => SelectedMods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005));
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("double time speed changed to 1.05x", () => SelectedMods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
|
||||||
|
|
||||||
|
OsuModNightcore nc = new OsuModNightcore
|
||||||
|
{
|
||||||
|
SpeedChange = { Value = 1.05 }
|
||||||
|
};
|
||||||
|
AddStep("select NC", () => SelectedMods.Value = new[] { nc });
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("nightcore speed changed to 1.1x", () => SelectedMods.Value.OfType<ModNightcore>().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005));
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("nightcore speed changed to 1.05x", () => SelectedMods.Value.OfType<ModNightcore>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("no mods selected", () => SelectedMods.Value.Count == 0);
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("daycore activated at 0.95x", () => SelectedMods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("daycore activated at 0.95x", () => SelectedMods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005));
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("daycore activated at 0.95x", () => SelectedMods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
|
||||||
|
|
||||||
|
OsuModDoubleTime dt = new OsuModDoubleTime
|
||||||
|
{
|
||||||
|
SpeedChange = { Value = 1.02 },
|
||||||
|
AdjustPitch = { Value = true },
|
||||||
|
};
|
||||||
|
AddStep("select DT", () => SelectedMods.Value = new[] { dt });
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("half time activated at 0.97x", () => SelectedMods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.97).Within(0.005));
|
||||||
|
AddAssert("adjust pitch preserved", () => SelectedMods.Value.OfType<ModHalfTime>().Single().AdjustPitch.Value, () => Is.True);
|
||||||
|
|
||||||
|
OsuModHalfTime ht = new OsuModHalfTime
|
||||||
|
{
|
||||||
|
SpeedChange = { Value = 0.97 },
|
||||||
|
AdjustPitch = { Value = true },
|
||||||
|
};
|
||||||
|
Mod[] modlist = { ht, new OsuModHardRock(), new OsuModHidden() };
|
||||||
|
AddStep("select HT+HD", () => SelectedMods.Value = modlist);
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("double time activated at 1.02x", () => SelectedMods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.02).Within(0.005));
|
||||||
|
AddAssert("double time activated at 1.02x", () => SelectedMods.Value.OfType<ModDoubleTime>().Single().AdjustPitch.Value, () => Is.True);
|
||||||
|
AddAssert("HD still enabled", () => SelectedMods.Value.OfType<ModHidden>().SingleOrDefault(), () => Is.Not.Null);
|
||||||
|
AddAssert("HR still enabled", () => SelectedMods.Value.OfType<ModHardRock>().SingleOrDefault(), () => Is.Not.Null);
|
||||||
|
|
||||||
|
AddStep("select WU", () => SelectedMods.Value = new[] { new ModWindUp() });
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("windup still active", () => SelectedMods.Value.First() is ModWindUp);
|
||||||
|
|
||||||
|
AddStep("select AS", () => SelectedMods.Value = new[] { new ModAdaptiveSpeed() });
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("adaptive speed still active", () => SelectedMods.Value.First() is ModAdaptiveSpeed);
|
||||||
|
|
||||||
|
OsuModDoubleTime dtWithAdjustPitch = new OsuModDoubleTime
|
||||||
|
{
|
||||||
|
SpeedChange = { Value = 1.05 },
|
||||||
|
AdjustPitch = { Value = true },
|
||||||
|
};
|
||||||
|
AddStep("select DT x1.05", () => SelectedMods.Value = new[] { dtWithAdjustPitch });
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("no mods selected", () => SelectedMods.Value.Count == 0);
|
||||||
|
|
||||||
|
decreaseModSpeed();
|
||||||
|
AddAssert("half time activated at 0.95x", () => SelectedMods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
|
||||||
|
AddAssert("half time has adjust pitch active", () => SelectedMods.Value.OfType<ModHalfTime>().Single().AdjustPitch.Value, () => Is.True);
|
||||||
|
|
||||||
|
AddStep("turn off adjust pitch", () => SelectedMods.Value.OfType<ModHalfTime>().Single().AdjustPitch.Value = false);
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("no mods selected", () => SelectedMods.Value.Count == 0);
|
||||||
|
|
||||||
|
increaseModSpeed();
|
||||||
|
AddAssert("double time activated at 1.05x", () => SelectedMods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
|
||||||
|
AddAssert("double time has adjust pitch inactive", () => SelectedMods.Value.OfType<ModDoubleTime>().Single().AdjustPitch.Value, () => Is.False);
|
||||||
|
|
||||||
|
void increaseModSpeed() => AddStep("increase mod speed", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Up);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
void decreaseModSpeed() => AddStep("decrease mod speed", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Down);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAutoplayShortcut()
|
||||||
|
{
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
|
||||||
|
LoadSongSelect();
|
||||||
|
|
||||||
|
// song select should automatically select the beatmap for us but this is not implemented yet.
|
||||||
|
// todo: remove when that's the case.
|
||||||
|
AddAssert("no beatmap selected", () => Beatmap.IsDefault);
|
||||||
|
AddStep("select beatmap", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmaps.GetAllUsableBeatmapSets().Single().Beatmaps.First()));
|
||||||
|
AddStep("press right", () => InputManager.Key(Key.Right)); // press right to select in carousel, also remove.
|
||||||
|
AddAssert("beatmap selected", () => !Beatmap.IsDefault);
|
||||||
|
|
||||||
|
AddStep("press ctrl+enter", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Enter);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader);
|
||||||
|
|
||||||
|
AddAssert("autoplay selected", () => SongSelect.Mods.Value.Single() is ModAutoplay);
|
||||||
|
|
||||||
|
AddUntilStep("wait for return to ss", () => SongSelect.IsCurrentScreen());
|
||||||
|
|
||||||
|
AddAssert("no mods selected", () => SongSelect.Mods.Value.Count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAutoplayShortcutKeepsAutoplayIfSelectedAlready()
|
||||||
|
{
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
|
||||||
|
LoadSongSelect();
|
||||||
|
|
||||||
|
// song select should automatically select the beatmap for us but this is not implemented yet.
|
||||||
|
// todo: remove when that's the case.
|
||||||
|
AddAssert("no beatmap selected", () => Beatmap.IsDefault);
|
||||||
|
AddStep("select beatmap", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmaps.GetAllUsableBeatmapSets().Single().Beatmaps.First()));
|
||||||
|
AddStep("press right", () => InputManager.Key(Key.Right)); // press right to select in carousel, also remove.
|
||||||
|
AddAssert("beatmap selected", () => !Beatmap.IsDefault);
|
||||||
|
|
||||||
|
ChangeMods(new OsuModAutoplay());
|
||||||
|
|
||||||
|
AddStep("press ctrl+enter", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Enter);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader);
|
||||||
|
|
||||||
|
AddAssert("autoplay selected", () => SongSelect.Mods.Value.Single() is ModAutoplay);
|
||||||
|
|
||||||
|
AddUntilStep("wait for return to ss", () => SongSelect.IsCurrentScreen());
|
||||||
|
|
||||||
|
AddAssert("autoplay still selected", () => SongSelect.Mods.Value.Single() is ModAutoplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAutoplayShortcutReturnsInitialModsOnExit()
|
||||||
|
{
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
|
||||||
|
LoadSongSelect();
|
||||||
|
|
||||||
|
// song select should automatically select the beatmap for us but this is not implemented yet.
|
||||||
|
// todo: remove when that's the case.
|
||||||
|
AddAssert("no beatmap selected", () => Beatmap.IsDefault);
|
||||||
|
AddStep("select beatmap", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmaps.GetAllUsableBeatmapSets().Single().Beatmaps.First()));
|
||||||
|
AddStep("press right", () => InputManager.Key(Key.Right)); // press right to select in carousel, also remove.
|
||||||
|
AddAssert("beatmap selected", () => !Beatmap.IsDefault);
|
||||||
|
|
||||||
|
ChangeMods(new OsuModRelax());
|
||||||
|
|
||||||
|
AddStep("press ctrl+enter", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Enter);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader);
|
||||||
|
|
||||||
|
AddAssert("only autoplay selected", () => SongSelect.Mods.Value.Single() is ModAutoplay);
|
||||||
|
|
||||||
|
AddUntilStep("wait for return to ss", () => SongSelect.IsCurrentScreen());
|
||||||
|
|
||||||
|
AddAssert("relax returned", () => SongSelect.Mods.Value.Single() is ModRelax);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Footer
|
#region Footer
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestMods()
|
public void TestFooterMods()
|
||||||
{
|
{
|
||||||
|
LoadSongSelect();
|
||||||
|
|
||||||
AddStep("one mod", () => SelectedMods.Value = new List<Mod> { new OsuModHidden() });
|
AddStep("one mod", () => SelectedMods.Value = new List<Mod> { new OsuModHidden() });
|
||||||
AddStep("two mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock() });
|
AddStep("two mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock() });
|
||||||
AddStep("three mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() });
|
AddStep("three mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() });
|
||||||
@@ -121,27 +373,25 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestShowOptions()
|
public void TestFooterModOverlay()
|
||||||
{
|
{
|
||||||
AddStep("enable options", () =>
|
LoadSongSelect();
|
||||||
|
|
||||||
|
AddStep("Press F1", () =>
|
||||||
{
|
{
|
||||||
var optionsButton = this.ChildrenOfType<ScreenFooterButton>().Last();
|
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonMods>().Single());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
optionsButton.Enabled.Value = true;
|
|
||||||
optionsButton.TriggerClick();
|
|
||||||
});
|
});
|
||||||
}
|
AddAssert("Overlay visible", () => this.ChildrenOfType<ModSelectOverlay>().Single().State.Value == Visibility.Visible);
|
||||||
|
AddStep("Hide", () => this.ChildrenOfType<ModSelectOverlay>().Single().Hide());
|
||||||
[Test]
|
|
||||||
public void TestState()
|
|
||||||
{
|
|
||||||
AddToggleStep("set options enabled state", state => this.ChildrenOfType<ScreenFooterButton>().Last().Enabled.Value = state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add these test cases when functionality is implemented.
|
// add these test cases when functionality is implemented.
|
||||||
// [Test]
|
// [Test]
|
||||||
// public void TestFooterRandom()
|
// public void TestFooterRandom()
|
||||||
// {
|
// {
|
||||||
|
// loadSongSelect();
|
||||||
|
//
|
||||||
// AddStep("press F2", () => InputManager.Key(Key.F2));
|
// AddStep("press F2", () => InputManager.Key(Key.F2));
|
||||||
// AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
|
// AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
|
||||||
// }
|
// }
|
||||||
@@ -149,6 +399,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
// [Test]
|
// [Test]
|
||||||
// public void TestFooterRandomViaMouse()
|
// public void TestFooterRandomViaMouse()
|
||||||
// {
|
// {
|
||||||
|
// loadSongSelect();
|
||||||
|
//
|
||||||
// AddStep("click button", () =>
|
// AddStep("click button", () =>
|
||||||
// {
|
// {
|
||||||
// InputManager.MoveMouseTo(randomButton);
|
// InputManager.MoveMouseTo(randomButton);
|
||||||
@@ -160,6 +412,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
// [Test]
|
// [Test]
|
||||||
// public void TestFooterRewind()
|
// public void TestFooterRewind()
|
||||||
// {
|
// {
|
||||||
|
// loadSongSelect();
|
||||||
|
//
|
||||||
// AddStep("press Shift+F2", () =>
|
// AddStep("press Shift+F2", () =>
|
||||||
// {
|
// {
|
||||||
// InputManager.PressKey(Key.LShift);
|
// InputManager.PressKey(Key.LShift);
|
||||||
@@ -173,6 +427,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
// [Test]
|
// [Test]
|
||||||
// public void TestFooterRewindViaShiftMouseLeft()
|
// public void TestFooterRewindViaShiftMouseLeft()
|
||||||
// {
|
// {
|
||||||
|
// loadSongSelect();
|
||||||
|
//
|
||||||
// AddStep("shift + click button", () =>
|
// AddStep("shift + click button", () =>
|
||||||
// {
|
// {
|
||||||
// InputManager.PressKey(Key.LShift);
|
// InputManager.PressKey(Key.LShift);
|
||||||
@@ -186,6 +442,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
// [Test]
|
// [Test]
|
||||||
// public void TestFooterRewindViaMouseRight()
|
// public void TestFooterRewindViaMouseRight()
|
||||||
// {
|
// {
|
||||||
|
// loadSongSelect();
|
||||||
|
//
|
||||||
// AddStep("right click button", () =>
|
// AddStep("right click button", () =>
|
||||||
// {
|
// {
|
||||||
// InputManager.MoveMouseTo(randomButton);
|
// InputManager.MoveMouseTo(randomButton);
|
||||||
@@ -195,31 +453,46 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestOverlayPresent()
|
public void TestFooterOptions()
|
||||||
{
|
{
|
||||||
AddStep("Press F1", () =>
|
LoadSongSelect();
|
||||||
{
|
|
||||||
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonMods>().Single());
|
ImportBeatmapForRuleset(0);
|
||||||
InputManager.Click(MouseButton.Left);
|
|
||||||
});
|
// song select should automatically select the beatmap for us but this is not implemented yet.
|
||||||
AddAssert("Overlay visible", () => this.ChildrenOfType<ModSelectOverlay>().Single().State.Value == Visibility.Visible);
|
// todo: remove when that's the case.
|
||||||
AddStep("Hide", () => this.ChildrenOfType<ModSelectOverlay>().Single().Hide());
|
AddAssert("no beatmap selected", () => Beatmap.IsDefault);
|
||||||
|
AddStep("select beatmap", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmaps.GetAllUsableBeatmapSets().Single().Beatmaps.First()));
|
||||||
|
AddAssert("options enabled", () => this.ChildrenOfType<FooterButtonOptions>().Single().Enabled.Value);
|
||||||
|
|
||||||
|
AddStep("click", () => this.ChildrenOfType<FooterButtonOptions>().Single().TriggerClick());
|
||||||
|
AddUntilStep("popover displayed", () => this.ChildrenOfType<FooterButtonOptions.Popover>().Any(p => p.IsPresent));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestFooterOptionsState()
|
||||||
|
{
|
||||||
|
LoadSongSelect();
|
||||||
|
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
|
||||||
|
// song select should automatically select the beatmap for us but this is not implemented yet.
|
||||||
|
// todo: remove when that's the case.
|
||||||
|
AddAssert("no beatmap selected", () => Beatmap.IsDefault);
|
||||||
|
AddStep("select beatmap", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmaps.GetAllUsableBeatmapSets().Single().Beatmaps.First()));
|
||||||
|
|
||||||
|
AddAssert("options enabled", () => this.ChildrenOfType<FooterButtonOptions>().Single().Enabled.Value);
|
||||||
|
AddStep("delete all beatmaps", () => Beatmaps.Delete());
|
||||||
|
|
||||||
|
// song select should automatically select the beatmap for us but this is not implemented yet.
|
||||||
|
// todo: remove when that's the case.
|
||||||
|
AddAssert("beatmap selected", () => !Beatmap.IsDefault);
|
||||||
|
AddStep("select no beatmap", () => Beatmap.SetDefault());
|
||||||
|
|
||||||
|
AddUntilStep("wait for no beatmap", () => Beatmap.IsDefault);
|
||||||
|
AddAssert("options disabled", () => !this.ChildrenOfType<FooterButtonOptions>().Single().Enabled.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private void updateFooter(IScreen? _, IScreen? newScreen)
|
|
||||||
{
|
|
||||||
if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter)
|
|
||||||
{
|
|
||||||
screenScreenFooter.Show();
|
|
||||||
screenScreenFooter.SetButtons(osuScreen.CreateFooterButtons());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
screenScreenFooter.Hide();
|
|
||||||
screenScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Screens;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Online.Chat;
|
||||||
|
using osu.Game.Rulesets.Mania.Mods;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
using osu.Game.Screens.Select.Filter;
|
||||||
|
using FilterControl = osu.Game.Screens.SelectV2.FilterControl;
|
||||||
|
using NoResultsPlaceholder = osu.Game.Screens.SelectV2.NoResultsPlaceholder;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||||
|
{
|
||||||
|
public partial class TestSceneSongSelectFiltering : SongSelectTestScene
|
||||||
|
{
|
||||||
|
private FilterControl filter => SongSelect.ChildrenOfType<FilterControl>().Single();
|
||||||
|
private ShearedFilterTextBox filterTextBox => SongSelect.ChildrenOfType<ShearedFilterTextBox>().Single();
|
||||||
|
private int filterOperationsCount;
|
||||||
|
|
||||||
|
protected override void LoadSongSelect()
|
||||||
|
{
|
||||||
|
base.LoadSongSelect();
|
||||||
|
|
||||||
|
AddStep("hook filter event", () =>
|
||||||
|
{
|
||||||
|
filterOperationsCount = 0;
|
||||||
|
filter.CriteriaChanged += _ => filterOperationsCount++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSingleFilterOnEnter()
|
||||||
|
{
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
|
||||||
|
LoadSongSelect();
|
||||||
|
|
||||||
|
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNoFilterOnSimpleResume()
|
||||||
|
{
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
|
||||||
|
LoadSongSelect();
|
||||||
|
|
||||||
|
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
|
||||||
|
WaitForSuspension();
|
||||||
|
|
||||||
|
AddStep("return", () => SongSelect.MakeCurrent());
|
||||||
|
AddUntilStep("wait for current", () => SongSelect.IsCurrentScreen());
|
||||||
|
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestFilterOnResumeAfterChange()
|
||||||
|
{
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
|
||||||
|
AddStep("change convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
|
||||||
|
|
||||||
|
LoadSongSelect();
|
||||||
|
|
||||||
|
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
|
||||||
|
WaitForSuspension();
|
||||||
|
|
||||||
|
AddStep("change convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
|
||||||
|
|
||||||
|
AddStep("return", () => SongSelect.MakeCurrent());
|
||||||
|
AddUntilStep("wait for current", () => SongSelect.IsCurrentScreen());
|
||||||
|
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSorting()
|
||||||
|
{
|
||||||
|
LoadSongSelect();
|
||||||
|
AddManyTestMaps();
|
||||||
|
|
||||||
|
// TODO: old test has this step, but there doesn't seem to be any purpose for it.
|
||||||
|
// AddUntilStep("random map selected", () => Beatmap.Value != defaultBeatmap);
|
||||||
|
|
||||||
|
AddStep(@"Sort by Artist", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
|
||||||
|
AddStep(@"Sort by Title", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
|
||||||
|
AddStep(@"Sort by Author", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Author));
|
||||||
|
AddStep(@"Sort by DateAdded", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.DateAdded));
|
||||||
|
AddStep(@"Sort by BPM", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.BPM));
|
||||||
|
AddStep(@"Sort by Length", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Length));
|
||||||
|
AddStep(@"Sort by Difficulty", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Difficulty));
|
||||||
|
AddStep(@"Sort by Source", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Source));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCutInFilterTextBox()
|
||||||
|
{
|
||||||
|
LoadSongSelect();
|
||||||
|
|
||||||
|
AddStep("set filter text", () => filterTextBox.Current.Value = "nonono");
|
||||||
|
AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll));
|
||||||
|
AddStep("press ctrl/cmd-x", () => InputManager.Keys(PlatformAction.Cut));
|
||||||
|
|
||||||
|
AddAssert("filter text cleared", () => filterTextBox.Current.Value, () => Is.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNonFilterableModChange()
|
||||||
|
{
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
|
||||||
|
LoadSongSelect();
|
||||||
|
|
||||||
|
// Mod that is guaranteed to never re-filter.
|
||||||
|
AddStep("add non-filterable mod", () => SelectedMods.Value = new Mod[] { new OsuModCinema() });
|
||||||
|
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||||
|
|
||||||
|
// Removing the mod should still not re-filter.
|
||||||
|
AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||||
|
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestFilterableModChange()
|
||||||
|
{
|
||||||
|
ImportBeatmapForRuleset(3);
|
||||||
|
|
||||||
|
LoadSongSelect();
|
||||||
|
|
||||||
|
// Change to mania ruleset.
|
||||||
|
AddStep("filter to mania ruleset", () => Ruleset.Value = Rulesets.AvailableRulesets.First(r => r.OnlineID == 3));
|
||||||
|
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
// Apply a mod, but this should NOT re-filter because there's no search text.
|
||||||
|
AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() });
|
||||||
|
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
// Set search text. Should re-filter.
|
||||||
|
AddStep("set search text to match mods", () => filterTextBox.Current.Value = "keys=3");
|
||||||
|
AddAssert("filter count is 2", () => filterOperationsCount, () => Is.EqualTo(2));
|
||||||
|
|
||||||
|
// Change filterable mod. Should re-filter.
|
||||||
|
AddStep("change new filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey5() });
|
||||||
|
AddAssert("filter count is 3", () => filterOperationsCount, () => Is.EqualTo(3));
|
||||||
|
|
||||||
|
// Add non-filterable mod. Should NOT re-filter.
|
||||||
|
AddStep("apply non-filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail(), new ManiaModKey5() });
|
||||||
|
AddAssert("filter count is 3", () => filterOperationsCount, () => Is.EqualTo(3));
|
||||||
|
|
||||||
|
// Remove filterable mod. Should re-filter.
|
||||||
|
AddStep("remove filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail() });
|
||||||
|
AddAssert("filter count is 4", () => filterOperationsCount, () => Is.EqualTo(4));
|
||||||
|
|
||||||
|
// Remove non-filterable mod. Should NOT re-filter.
|
||||||
|
AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||||
|
AddAssert("filter count is 4", () => filterOperationsCount, () => Is.EqualTo(4));
|
||||||
|
|
||||||
|
// Add filterable mod. Should re-filter.
|
||||||
|
AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() });
|
||||||
|
AddAssert("filter count is 5", () => filterOperationsCount, () => Is.EqualTo(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlaceholderVisibleAfterDeleteAll()
|
||||||
|
{
|
||||||
|
LoadSongSelect();
|
||||||
|
|
||||||
|
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||||
|
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
AddUntilStep("wait for placeholder hidden", () => getPlaceholder()?.State.Value == Visibility.Hidden);
|
||||||
|
|
||||||
|
AddStep("delete all beatmaps", () => Beatmaps.Delete());
|
||||||
|
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlaceholderVisibleAfterStarDifficultyFilter()
|
||||||
|
{
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
AddStep("change star filter", () => Config.SetValue(OsuSetting.DisplayStarsMinimum, 10.0));
|
||||||
|
|
||||||
|
LoadSongSelect();
|
||||||
|
|
||||||
|
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||||
|
|
||||||
|
AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType<DrawableLinkCompiler>().First().TriggerClick());
|
||||||
|
|
||||||
|
AddUntilStep("star filter reset", () => Config.Get<double>(OsuSetting.DisplayStarsMinimum) == 0.0);
|
||||||
|
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlaceholderVisibleWithConvertSetting()
|
||||||
|
{
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
AddStep("change convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
|
||||||
|
|
||||||
|
LoadSongSelect();
|
||||||
|
|
||||||
|
ChangeRuleset(2);
|
||||||
|
|
||||||
|
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||||
|
|
||||||
|
AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType<DrawableLinkCompiler>().First().TriggerClick());
|
||||||
|
|
||||||
|
AddUntilStep("convert setting changed", () => Config.Get<bool>(OsuSetting.ShowConvertedBeatmaps));
|
||||||
|
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCorrectMatchCountAfterDeleteAll()
|
||||||
|
{
|
||||||
|
LoadSongSelect();
|
||||||
|
checkMatchedBeatmaps(0);
|
||||||
|
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
checkMatchedBeatmaps(3);
|
||||||
|
|
||||||
|
AddStep("delete all beatmaps", () => Beatmaps.Delete());
|
||||||
|
checkMatchedBeatmaps(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCorrectMatchCountAfterHardDelete()
|
||||||
|
{
|
||||||
|
LoadSongSelect();
|
||||||
|
checkMatchedBeatmaps(0);
|
||||||
|
|
||||||
|
ImportBeatmapForRuleset(0);
|
||||||
|
checkMatchedBeatmaps(3);
|
||||||
|
|
||||||
|
AddStep("hard delete beatmap", () => Realm.Write(r => r.RemoveRange(r.All<BeatmapSetInfo>().Where(s => !s.Protected))));
|
||||||
|
checkMatchedBeatmaps(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NoResultsPlaceholder? getPlaceholder() => SongSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault();
|
||||||
|
|
||||||
|
private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System.Linq;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using osu.Framework.Testing;
|
|
||||||
using osu.Game.Screens.Menu;
|
|
||||||
using osuTK.Input;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
|
||||||
{
|
|
||||||
public partial class TestSceneSongSelectNavigation : OsuGameTestScene
|
|
||||||
{
|
|
||||||
public override void SetUpSteps()
|
|
||||||
{
|
|
||||||
base.SetUpSteps();
|
|
||||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
|
||||||
AddWaitStep("wait", 5);
|
|
||||||
PushAndConfirm(() => new Screens.SelectV2.SoloSongSelect());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestClickLogo()
|
|
||||||
{
|
|
||||||
AddStep("click", () =>
|
|
||||||
{
|
|
||||||
InputManager.MoveMouseTo(Game.ChildrenOfType<OsuLogo>().Single());
|
|
||||||
InputManager.Click(MouseButton.Left);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
59
osu.Game.Tests/Visual/TestSceneReplayStability.cs
Normal file
59
osu.Game.Tests/Visual/TestSceneReplayStability.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Replays;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Rulesets.Replays;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual
|
||||||
|
{
|
||||||
|
public partial class TestSceneReplayStability : ReplayStabilityTestScene
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestOutrageouslyLargeLeadInTime()
|
||||||
|
{
|
||||||
|
// "graciously borrowed" from https://osu.ppy.sh/beatmapsets/948643#osu/1981090
|
||||||
|
const double lead_in_time = 2147272727;
|
||||||
|
const double hit_circle_time = 100;
|
||||||
|
|
||||||
|
var beatmap = new OsuBeatmap
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = hit_circle_time,
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AudioLeadIn = lead_in_time,
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var replay = new Replay
|
||||||
|
{
|
||||||
|
Frames = Enumerable.Range(0, 300).Select(t => new OsuReplayFrame(-lead_in_time + 40 * t, new Vector2(t), t % 2 == 0 ? [] : [OsuAction.LeftButton]))
|
||||||
|
.Concat([
|
||||||
|
new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2),
|
||||||
|
new OsuReplayFrame(hit_circle_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
|
||||||
|
new OsuReplayFrame(hit_circle_time + 20, OsuPlayfield.BASE_SIZE / 2),
|
||||||
|
])
|
||||||
|
.Cast<ReplayFrame>()
|
||||||
|
.ToList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
RunTest(beatmap, replay, [HitResult.Great]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,9 +13,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
{
|
{
|
||||||
public partial class TestSceneBackButton : OsuTestScene
|
public partial class TestSceneBackButton : OsuTestScene
|
||||||
{
|
{
|
||||||
|
private readonly BackButton? button;
|
||||||
|
|
||||||
public TestSceneBackButton()
|
public TestSceneBackButton()
|
||||||
{
|
{
|
||||||
BackButton button;
|
|
||||||
ScreenFooter.BackReceptor receptor = new ScreenFooter.BackReceptor();
|
ScreenFooter.BackReceptor receptor = new ScreenFooter.BackReceptor();
|
||||||
|
|
||||||
Child = new Container
|
Child = new Container
|
||||||
@@ -34,14 +35,13 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
},
|
},
|
||||||
button = new BackButton(receptor)
|
button = new BackButton(receptor)
|
||||||
{
|
{
|
||||||
|
Action = () => button?.Hide(),
|
||||||
Anchor = Anchor.BottomLeft,
|
Anchor = Anchor.BottomLeft,
|
||||||
Origin = Anchor.BottomLeft,
|
Origin = Anchor.BottomLeft,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
button.Action = () => button.Hide();
|
|
||||||
|
|
||||||
AddStep("show button", () => button.Show());
|
AddStep("show button", () => button.Show());
|
||||||
AddStep("hide button", () => button.Hide());
|
AddStep("hide button", () => button.Hide());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
@@ -16,7 +17,7 @@ using osu.Game.Screens.Play.HUD;
|
|||||||
|
|
||||||
namespace osu.Game.Tests.Visual.UserInterface
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
{
|
{
|
||||||
public partial class TestSceneModIcon : OsuTestScene
|
public partial class TestSceneModIcon : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
private FillFlowContainer spreadOutFlow = null!;
|
private FillFlowContainer spreadOutFlow = null!;
|
||||||
private ModDisplay modDisplay = null!;
|
private ModDisplay modDisplay = null!;
|
||||||
@@ -181,5 +182,40 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTooltip()
|
||||||
|
{
|
||||||
|
OsuModDoubleTime mod = null!;
|
||||||
|
|
||||||
|
AddStep("create icon", () => addRange([mod = new OsuModDoubleTime()]));
|
||||||
|
AddStep("hover", () => InputManager.MoveMouseTo(this.ChildrenOfType<ModIcon>().First()));
|
||||||
|
AddUntilStep("tooltip displayed", () => getTooltip()?.IsPresent, () => Is.True);
|
||||||
|
AddAssert("tooltip text = \"Double Time\"", getTooltipText, () => Is.EqualTo("Double Time"));
|
||||||
|
AddAssert("tooltip settings empty", () => getTooltipSettingsLabels().Concat(getTooltipSettingsValues()), () => Is.Empty);
|
||||||
|
|
||||||
|
AddStep("change settings", () => mod.SpeedChange.Value = 1.75f);
|
||||||
|
AddAssert("tooltip text = \"Double Time\"", getTooltipText, () => Is.EqualTo("Double Time"));
|
||||||
|
AddAssert("tooltip settings updated",
|
||||||
|
() => getTooltipSettingsLabels().Concat(getTooltipSettingsValues()),
|
||||||
|
() => Is.EquivalentTo(new[] { "Speed ", "change", "1.75x" }));
|
||||||
|
|
||||||
|
AddStep("change settings", () => mod.SpeedChange.Value = 1.25f);
|
||||||
|
AddAssert("tooltip text = \"Double Time\"", getTooltipText, () => Is.EqualTo("Double Time"));
|
||||||
|
AddAssert("tooltip settings updated",
|
||||||
|
() => getTooltipSettingsLabels().Concat(getTooltipSettingsValues()),
|
||||||
|
() => Is.EquivalentTo(new[] { "Speed ", "change", "1.25x" }));
|
||||||
|
|
||||||
|
AddStep("rest settings", () => mod.SpeedChange.SetDefault());
|
||||||
|
AddAssert("tooltip text = \"Double Time\"", getTooltipText, () => Is.EqualTo("Double Time"));
|
||||||
|
AddAssert("tooltip settings empty", () => getTooltipSettingsLabels().Concat(getTooltipSettingsValues()), () => Is.Empty);
|
||||||
|
|
||||||
|
ModTooltip? getTooltip() => this.ChildrenOfType<ModTooltip>().SingleOrDefault();
|
||||||
|
|
||||||
|
// we could also just expose those directly from ModTooltip, but this works.
|
||||||
|
string getTooltipText() => getTooltip().ChildrenOfType<SpriteText>().First().Text.ToString();
|
||||||
|
IEnumerable<string> getTooltipSettingsLabels() => getTooltip().ChildrenOfType<TextFlowContainer>().First().ChildrenOfType<SpriteText>().Select(t => t.Text.ToString());
|
||||||
|
IEnumerable<string> getTooltipSettingsValues() => getTooltip().ChildrenOfType<TextFlowContainer>().Last().ChildrenOfType<SpriteText>().Select(t => t.Text.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Width = 0.5f,
|
Width = 0.5f,
|
||||||
FilterText = "12345 matches",
|
StatusText = "12345 matches",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -72,58 +72,66 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
first.PerformWrite(updated =>
|
first.PerformWrite(updated =>
|
||||||
{
|
{
|
||||||
var realm = updated.Realm;
|
try
|
||||||
|
|
||||||
Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database);
|
|
||||||
|
|
||||||
// Re-fetch as we are likely on a different thread.
|
|
||||||
original = realm!.Find<BeatmapSetInfo>(originalId)!;
|
|
||||||
|
|
||||||
// Generally the import process will do this for us if the OnlineIDs match,
|
|
||||||
// but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated).
|
|
||||||
original.DeletePending = true;
|
|
||||||
|
|
||||||
// Transfer local values which should be persisted across a beatmap update.
|
|
||||||
updated.DateAdded = originalDateAdded;
|
|
||||||
|
|
||||||
transferCollectionReferences(realm, original, updated);
|
|
||||||
|
|
||||||
foreach (var beatmap in original.Beatmaps.ToArray())
|
|
||||||
{
|
{
|
||||||
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.Hash);
|
var realm = updated.Realm;
|
||||||
|
|
||||||
if (updatedBeatmap != null)
|
// Re-fetch as we are likely on a different thread.
|
||||||
|
original = realm!.Find<BeatmapSetInfo>(originalId)!;
|
||||||
|
|
||||||
|
// Generally the import process will do this for us if the OnlineIDs match,
|
||||||
|
// but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated).
|
||||||
|
original.DeletePending = true;
|
||||||
|
|
||||||
|
// Transfer local values which should be persisted across a beatmap update.
|
||||||
|
updated.DateAdded = originalDateAdded;
|
||||||
|
|
||||||
|
transferCollectionReferences(realm, original, updated);
|
||||||
|
|
||||||
|
foreach (var beatmap in original.Beatmaps.ToArray())
|
||||||
{
|
{
|
||||||
// If the updated beatmap matches an existing one, transfer any user data across..
|
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.Hash);
|
||||||
if (beatmap.Scores.Any())
|
|
||||||
|
if (updatedBeatmap != null)
|
||||||
{
|
{
|
||||||
Logger.Log($"Transferring {beatmap.Scores.Count()} scores for unchanged difficulty \"{beatmap}\"", LoggingTarget.Database);
|
// If the updated beatmap matches an existing one, transfer any user data across..
|
||||||
|
if (beatmap.Scores.Any())
|
||||||
|
{
|
||||||
|
Logger.Log($"Transferring {beatmap.Scores.Count()} scores for unchanged difficulty \"{beatmap}\"", LoggingTarget.Database);
|
||||||
|
|
||||||
foreach (var score in beatmap.Scores)
|
foreach (var score in beatmap.Scores)
|
||||||
score.BeatmapInfo = updatedBeatmap;
|
score.BeatmapInfo = updatedBeatmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..then nuke the old beatmap completely.
|
||||||
|
// this is done instead of a soft deletion to avoid a user potentially creating weird
|
||||||
|
// interactions, like restoring the outdated beatmap then updating a second time
|
||||||
|
// (causing user data to be wiped).
|
||||||
|
original.Beatmaps.Remove(beatmap);
|
||||||
|
|
||||||
|
realm.Remove(beatmap.Metadata);
|
||||||
|
realm.Remove(beatmap);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If the beatmap differs in the original, leave it in a soft-deleted state but reset online info.
|
||||||
|
// This caters to the case where a user has made modifications they potentially want to restore,
|
||||||
|
// but after restoring we want to ensure it can't be used to trigger an update of the beatmap.
|
||||||
|
beatmap.ResetOnlineInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ..then nuke the old beatmap completely.
|
|
||||||
// this is done instead of a soft deletion to avoid a user potentially creating weird
|
|
||||||
// interactions, like restoring the outdated beatmap then updating a second time
|
|
||||||
// (causing user data to be wiped).
|
|
||||||
original.Beatmaps.Remove(beatmap);
|
|
||||||
|
|
||||||
realm.Remove(beatmap.Metadata);
|
|
||||||
realm.Remove(beatmap);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// If the beatmap differs in the original, leave it in a soft-deleted state but reset online info.
|
|
||||||
// This caters to the case where a user has made modifications they potentially want to restore,
|
|
||||||
// but after restoring we want to ensure it can't be used to trigger an update of the beatmap.
|
|
||||||
beatmap.ResetOnlineInfo();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the original has no beatmaps left, delete the set as well.
|
||||||
|
if (!original.Beatmaps.Any())
|
||||||
|
realm.Remove(original);
|
||||||
|
|
||||||
|
Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ex, $"Failed to update beatmap \"{updated}\"", LoggingTarget.Database);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the original has no beatmaps left, delete the set as well.
|
|
||||||
if (!original.Beatmaps.Any())
|
|
||||||
realm.Remove(original);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return first;
|
return first;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace osu.Game.Configuration
|
|||||||
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.BeatmapWithStoryboard))]
|
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.BeatmapWithStoryboard))]
|
||||||
BeatmapWithStoryboard,
|
BeatmapWithStoryboard,
|
||||||
|
|
||||||
[Description("Use 'Textures/Webm/*.webm' file from local folders")]
|
[Description("Use 'Resource\\Webm\\*.webm' file from local folders")]
|
||||||
WebmSource,
|
WebmSource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ namespace osu.Game.Configuration
|
|||||||
{
|
{
|
||||||
// UI/selection defaults
|
// UI/selection defaults
|
||||||
SetDefault(OsuSetting.Ruleset, string.Empty);
|
SetDefault(OsuSetting.Ruleset, string.Empty);
|
||||||
SetDefault(OsuSetting.Skin, SkinInfo.EZ2_SKIN.ToString());
|
SetDefault(OsuSetting.Skin, SkinInfo.ARGON_SKIN.ToString());
|
||||||
|
|
||||||
SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Local);
|
SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Local);
|
||||||
SetDefault(OsuSetting.BeatmapDetailModsFilter, false);
|
SetDefault(OsuSetting.BeatmapDetailModsFilter, false);
|
||||||
@@ -157,7 +157,7 @@ namespace osu.Game.Configuration
|
|||||||
|
|
||||||
SetDefault(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised);
|
SetDefault(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised);
|
||||||
SetDefault(OsuSetting.HitMode, MUGHitMode.EZ2AC);
|
SetDefault(OsuSetting.HitMode, MUGHitMode.EZ2AC);
|
||||||
SetDefault(OsuSetting.ColumnWidth, 46, 9, 90, 1.0);
|
SetDefault(OsuSetting.ColumnWidth, 50, 9, 90, 1.0);
|
||||||
SetDefault(OsuSetting.SpecialFactor, 1, 0.1, 4, 0.1);
|
SetDefault(OsuSetting.SpecialFactor, 1, 0.1, 4, 0.1);
|
||||||
SetDefault(OsuSetting.AccuracyCutoffS, 0.95, 0.95, 1, 0.005);
|
SetDefault(OsuSetting.AccuracyCutoffS, 0.95, 0.95, 1, 0.005);
|
||||||
SetDefault(OsuSetting.AccuracyCutoffA, 0.9, 0.9, 1, 0.005);
|
SetDefault(OsuSetting.AccuracyCutoffA, 0.9, 0.9, 1, 0.005);
|
||||||
@@ -235,6 +235,8 @@ namespace osu.Game.Configuration
|
|||||||
|
|
||||||
SetDefault(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, true);
|
SetDefault(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, true);
|
||||||
SetDefault(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, true);
|
SetDefault(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, true);
|
||||||
|
|
||||||
|
SetDefault(OsuSetting.WasSupporter, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
|
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
|
||||||
@@ -486,5 +488,11 @@ namespace osu.Game.Configuration
|
|||||||
EditorShowStoryboard,
|
EditorShowStoryboard,
|
||||||
EditorSubmissionNotifyOnDiscussionReplies,
|
EditorSubmissionNotifyOnDiscussionReplies,
|
||||||
EditorSubmissionLoadInBrowserAfterSubmission,
|
EditorSubmissionLoadInBrowserAfterSubmission,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cached state of whether local user is a supporter.
|
||||||
|
/// Used to allow early checks (ie for startup samples) to be in the correct state, even if the API authentication process has not completed.
|
||||||
|
/// </summary>
|
||||||
|
WasSupporter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ using osu.Framework.Logging;
|
|||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@@ -35,6 +36,11 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
{
|
{
|
||||||
#region Properties and methods for external usage
|
#region Properties and methods for external usage
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called after a filter operation or change in items results in the visible carousel items changing.
|
||||||
|
/// </summary>
|
||||||
|
public Action? NewItemsPresented { private get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it.
|
/// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -68,7 +74,7 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
public int ItemsTracked => Items.Count;
|
public int ItemsTracked => Items.Count;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The number of carousel items currently in rotation for display.
|
/// The items currently in rotation for display.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int DisplayableItems => carouselItems?.Count ?? 0;
|
public int DisplayableItems => carouselItems?.Count ?? 0;
|
||||||
|
|
||||||
@@ -164,7 +170,12 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Queue an asynchronous filter operation.
|
/// Queue an asynchronous filter operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual Task FilterAsync() => filterTask = performFilter();
|
protected virtual Task<IEnumerable<CarouselItem>> FilterAsync()
|
||||||
|
{
|
||||||
|
filterTask = performFilter();
|
||||||
|
filterTask.FireAndForget();
|
||||||
|
return filterTask;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check whether two models are the same for display purposes.
|
/// Check whether two models are the same for display purposes.
|
||||||
@@ -246,10 +257,10 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
|
|
||||||
private List<CarouselItem>? carouselItems;
|
private List<CarouselItem>? carouselItems;
|
||||||
|
|
||||||
private Task filterTask = Task.CompletedTask;
|
private Task<IEnumerable<CarouselItem>> filterTask = Task.FromResult(Enumerable.Empty<CarouselItem>());
|
||||||
private CancellationTokenSource cancellationSource = new CancellationTokenSource();
|
private CancellationTokenSource cancellationSource = new CancellationTokenSource();
|
||||||
|
|
||||||
private async Task performFilter()
|
private async Task<IEnumerable<CarouselItem>> performFilter()
|
||||||
{
|
{
|
||||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
@@ -265,7 +276,7 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
|
|
||||||
// Copy must be performed on update thread for now (see ConfigureAwait above).
|
// Copy must be performed on update thread for now (see ConfigureAwait above).
|
||||||
// Could potentially be optimised in the future if it becomes an issue.
|
// Could potentially be optimised in the future if it becomes an issue.
|
||||||
IEnumerable<CarouselItem> items = new List<CarouselItem>(Items.Select(m => new CarouselItem(m)));
|
List<CarouselItem> items = new List<CarouselItem>(Items.Select(m => new CarouselItem(m)));
|
||||||
|
|
||||||
await Task.Run(async () =>
|
await Task.Run(async () =>
|
||||||
{
|
{
|
||||||
@@ -275,6 +286,11 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
{
|
{
|
||||||
log($"Performing {filter.GetType().ReadableName()}");
|
log($"Performing {filter.GetType().ReadableName()}");
|
||||||
items = await filter.Run(items, cts.Token).ConfigureAwait(false);
|
items = await filter.Run(items, cts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// To avoid shooting ourselves in the foot, ensure that we manifest a list after each filter.
|
||||||
|
//
|
||||||
|
// A future improvement may be passing a reference list through each filter rather than copying each time,
|
||||||
|
// but this is the safest approach.
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Updating Y positions");
|
log("Updating Y positions");
|
||||||
@@ -287,20 +303,26 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
}, cts.Token).ConfigureAwait(false);
|
}, cts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
if (cts.Token.IsCancellationRequested)
|
if (cts.Token.IsCancellationRequested)
|
||||||
return;
|
return Enumerable.Empty<CarouselItem>();
|
||||||
|
|
||||||
Schedule(() =>
|
Schedule(() =>
|
||||||
{
|
{
|
||||||
log("Items ready for display");
|
log("Items ready for display");
|
||||||
carouselItems = items.ToList();
|
carouselItems = items;
|
||||||
displayedRange = null;
|
displayedRange = null;
|
||||||
|
|
||||||
// Need to call this to ensure correct post-selection logic is handled on the new items list.
|
// Need to call this to ensure correct post-selection logic is handled on the new items list.
|
||||||
HandleItemSelected(currentSelection.Model);
|
HandleItemSelected(currentSelection.Model);
|
||||||
|
|
||||||
refreshAfterSelection();
|
refreshAfterSelection();
|
||||||
|
if (!Scroll.UserScrolling)
|
||||||
|
scrollToSelection();
|
||||||
|
|
||||||
|
NewItemsPresented?.Invoke();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
|
||||||
void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}");
|
void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,13 +352,27 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
|
|
||||||
#region Input handling
|
#region Input handling
|
||||||
|
|
||||||
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
|
{
|
||||||
|
switch (e.Key)
|
||||||
|
{
|
||||||
|
// this is a special hard-coded case; we can't rely on OnPressed as GlobalActionContainer is
|
||||||
|
// matching with exact modifier consideration (so Ctrl+Enter would be ignored).
|
||||||
|
case Key.Enter:
|
||||||
|
case Key.KeypadEnter:
|
||||||
|
activateSelection();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnKeyDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
{
|
{
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
case GlobalAction.Select:
|
case GlobalAction.Select:
|
||||||
if (currentKeyboardSelection.CarouselItem != null)
|
activateSelection();
|
||||||
Activate(currentKeyboardSelection.CarouselItem);
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case GlobalAction.SelectNext:
|
case GlobalAction.SelectNext:
|
||||||
@@ -363,6 +399,12 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void activateSelection()
|
||||||
|
{
|
||||||
|
if (currentKeyboardSelection.CarouselItem != null)
|
||||||
|
Activate(currentKeyboardSelection.CarouselItem);
|
||||||
|
}
|
||||||
|
|
||||||
private void traverseKeyboardSelection(int direction)
|
private void traverseKeyboardSelection(int direction)
|
||||||
{
|
{
|
||||||
if (carouselItems == null || carouselItems.Count == 0) return;
|
if (carouselItems == null || carouselItems.Count == 0) return;
|
||||||
@@ -451,6 +493,9 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
|
|
||||||
#region Selection handling
|
#region Selection handling
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Becomes invalid when the current selection has changed and needs to be updated visually.
|
||||||
|
/// </summary>
|
||||||
private readonly Cached selectionValid = new Cached();
|
private readonly Cached selectionValid = new Cached();
|
||||||
|
|
||||||
private Selection currentKeyboardSelection = new Selection();
|
private Selection currentKeyboardSelection = new Selection();
|
||||||
@@ -551,7 +596,10 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
if (!selectionValid.IsValid)
|
if (!selectionValid.IsValid)
|
||||||
{
|
{
|
||||||
refreshAfterSelection();
|
refreshAfterSelection();
|
||||||
|
|
||||||
|
// Always scroll to selection in this case (regardless of `UserScrolling` state), centering the selection.
|
||||||
scrollToSelection();
|
scrollToSelection();
|
||||||
|
|
||||||
selectionValid.Validate();
|
selectionValid.Validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,7 +623,7 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
if (c.Item == null)
|
if (c.Item == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / DrawHeight);
|
float normalisedDepth = (float)(Math.Abs(selectedYPos - c.Item.CarouselYPosition) / DrawHeight);
|
||||||
Scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth);
|
Scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth);
|
||||||
|
|
||||||
if (c.DrawYPosition != c.Item.CarouselYPosition)
|
if (c.DrawYPosition != c.Item.CarouselYPosition)
|
||||||
@@ -647,6 +695,12 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
{
|
{
|
||||||
var carouselPanel = (ICarouselPanel)panel;
|
var carouselPanel = (ICarouselPanel)panel;
|
||||||
|
|
||||||
|
if (carouselPanel.Item == null)
|
||||||
|
{
|
||||||
|
// Item is null when a panel is already fading away from existence; should be ignored for tracking purposes.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// The case where we're intending to display this panel, but it's already displayed.
|
// The case where we're intending to display this panel, but it's already displayed.
|
||||||
// Note that we **must compare the model here** as the CarouselItems may be fresh instances due to a filter operation.
|
// Note that we **must compare the model here** as the CarouselItems may be fresh instances due to a filter operation.
|
||||||
//
|
//
|
||||||
@@ -662,7 +716,7 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the new display range doesn't contain the panel, it's no longer required for display.
|
// If the new display range doesn't contain the panel, it's no longer required for display.
|
||||||
expirePanelImmediately(panel);
|
expirePanel(panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add any new items which need to be displayed and haven't yet.
|
// Add any new items which need to be displayed and haven't yet.
|
||||||
@@ -673,12 +727,36 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
if (drawable is not ICarouselPanel carouselPanel)
|
if (drawable is not ICarouselPanel carouselPanel)
|
||||||
throw new InvalidOperationException($"Carousel panel drawables must implement {typeof(ICarouselPanel)}");
|
throw new InvalidOperationException($"Carousel panel drawables must implement {typeof(ICarouselPanel)}");
|
||||||
|
|
||||||
carouselPanel.DrawYPosition = item.CarouselYPosition;
|
|
||||||
carouselPanel.Item = item;
|
carouselPanel.Item = item;
|
||||||
|
|
||||||
Scroll.Add(drawable);
|
Scroll.Add(drawable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toDisplay.Any())
|
||||||
|
{
|
||||||
|
// To make transitions of items appearing in the flow look good, do a pass and make sure newly added items spawn from
|
||||||
|
// just beneath the *current interpolated position* of the previous panel.
|
||||||
|
var orderedPanels = Scroll.Panels
|
||||||
|
.OfType<ICarouselPanel>()
|
||||||
|
.Where(p => p.Item != null)
|
||||||
|
.OrderBy(p => p.Item!.CarouselYPosition)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
for (int i = 0; i < orderedPanels.Count; i++)
|
||||||
|
{
|
||||||
|
var panel = orderedPanels[i];
|
||||||
|
|
||||||
|
if (toDisplay.Contains(panel.Item!))
|
||||||
|
{
|
||||||
|
// Don't apply to the last because animating the tail of the list looks bad.
|
||||||
|
// It's usually off-screen anyway.
|
||||||
|
if (i > 0 && i < orderedPanels.Count - 1)
|
||||||
|
panel.DrawYPosition = orderedPanels[i - 1].DrawYPosition;
|
||||||
|
else
|
||||||
|
panel.DrawYPosition = panel.Item!.CarouselYPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update the total height of all items (to make the scroll container scrollable through the full height even though
|
// Update the total height of all items (to make the scroll container scrollable through the full height even though
|
||||||
// most items are not displayed / loaded).
|
// most items are not displayed / loaded).
|
||||||
if (carouselItems.Count > 0)
|
if (carouselItems.Count > 0)
|
||||||
@@ -690,13 +768,18 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
Scroll.SetLayoutHeight(0);
|
Scroll.SetLayoutHeight(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void expirePanelImmediately(Drawable panel)
|
private void expirePanel(Drawable panel)
|
||||||
{
|
{
|
||||||
panel.FinishTransforms();
|
|
||||||
panel.Expire();
|
|
||||||
|
|
||||||
var carouselPanel = (ICarouselPanel)panel;
|
var carouselPanel = (ICarouselPanel)panel;
|
||||||
|
|
||||||
|
// expired panels should have a depth behind all other panels to make the transition not look weird.
|
||||||
|
Scroll.Panels.ChangeChildDepth(panel, panel.Depth + 1024);
|
||||||
|
|
||||||
|
panel.FadeOut(150, Easing.OutQuint);
|
||||||
|
panel.MoveToX(panel.X + 100, 200, Easing.Out);
|
||||||
|
|
||||||
|
panel.Expire();
|
||||||
|
|
||||||
carouselPanel.Item = null;
|
carouselPanel.Item = null;
|
||||||
carouselPanel.Selected.Value = false;
|
carouselPanel.Selected.Value = false;
|
||||||
carouselPanel.KeyboardSelected.Value = false;
|
carouselPanel.KeyboardSelected.Value = false;
|
||||||
@@ -753,12 +836,7 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
base.OffsetScrollPosition(offset);
|
base.OffsetScrollPosition(offset);
|
||||||
|
|
||||||
foreach (var panel in Panels)
|
foreach (var panel in Panels)
|
||||||
{
|
((ICarouselPanel)panel).DrawYPosition += offset;
|
||||||
var c = (ICarouselPanel)panel;
|
|
||||||
Debug.Assert(c.Item != null);
|
|
||||||
|
|
||||||
c.DrawYPosition += offset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Clear(bool disposeChildren)
|
public override void Clear(bool disposeChildren)
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ namespace osu.Game.Graphics.Carousel
|
|||||||
/// <param name="items">The items to be filtered.</param>
|
/// <param name="items">The items to be filtered.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token.</param>
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
/// <returns>The post-filtered items.</returns>
|
/// <returns>The post-filtered items.</returns>
|
||||||
Task<IEnumerable<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken);
|
Task<List<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using osu.Framework.Extensions.MatrixExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Layout;
|
using osu.Framework.Layout;
|
||||||
@@ -54,7 +55,8 @@ namespace osu.Game.Graphics.Containers
|
|||||||
parentMatrix.M31 = 0.0f;
|
parentMatrix.M31 = 0.0f;
|
||||||
parentMatrix.M32 = 0.0f;
|
parentMatrix.M32 = 0.0f;
|
||||||
|
|
||||||
Matrix3 reversedParent = parentMatrix.Inverted();
|
Matrix3 reversedParent = parentMatrix;
|
||||||
|
MatrixExtensions.FastInvert(ref reversedParent);
|
||||||
|
|
||||||
// Extract the rotation.
|
// Extract the rotation.
|
||||||
float angle = MathF.Atan2(reversedParent.M12, reversedParent.M11);
|
float angle = MathF.Atan2(reversedParent.M12, reversedParent.M11);
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@@ -14,11 +12,11 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
// todo: remove this once all screens migrate to display the new game footer and back button.
|
// todo: remove this once all screens migrate to display the new game footer and back button.
|
||||||
public partial class BackButton : VisibilityContainer
|
public partial class BackButton : VisibilityContainer
|
||||||
{
|
{
|
||||||
public Action Action;
|
public Action? Action { get; init; }
|
||||||
|
|
||||||
private readonly TwoLayerButton button;
|
private readonly TwoLayerButton button;
|
||||||
|
|
||||||
public BackButton(ScreenFooter.BackReceptor receptor = null)
|
public BackButton(ScreenFooter.BackReceptor? receptor = null)
|
||||||
{
|
{
|
||||||
Size = TwoLayerButton.SIZE_EXTENDED;
|
Size = TwoLayerButton.SIZE_EXTENDED;
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
{
|
{
|
||||||
private const float filter_text_size = 12;
|
private const float filter_text_size = 12;
|
||||||
|
|
||||||
public LocalisableString FilterText
|
public LocalisableString StatusText
|
||||||
{
|
{
|
||||||
get => ((InnerFilterTextBox)TextBox).FilterText.Text;
|
get => ((InnerFilterTextBox)TextBox).StatusText.Text;
|
||||||
set => Schedule(() => ((InnerFilterTextBox)TextBox).FilterText.Text = value);
|
set => Schedule(() => ((InnerFilterTextBox)TextBox).StatusText.Text = value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ShearedFilterTextBox()
|
public ShearedFilterTextBox()
|
||||||
@@ -27,12 +27,12 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
|
|
||||||
protected partial class InnerFilterTextBox : InnerSearchTextBox
|
protected partial class InnerFilterTextBox : InnerSearchTextBox
|
||||||
{
|
{
|
||||||
public OsuSpriteText FilterText { get; private set; } = null!;
|
public OsuSpriteText StatusText { get; private set; } = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
TextContainer.Add(FilterText = new OsuSpriteText
|
TextContainer.Add(StatusText = new OsuSpriteText
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomLeft,
|
Anchor = Anchor.BottomLeft,
|
||||||
Origin = Anchor.TopLeft,
|
Origin = Anchor.TopLeft,
|
||||||
|
|||||||
@@ -135,6 +135,11 @@ Click to see what's new!", version);
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString DownloadingUpdate => new TranslatableString(getKey(@"downloading_update"), @"Downloading update...");
|
public static LocalisableString DownloadingUpdate => new TranslatableString(getKey(@"downloading_update"), @"Downloading update...");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "This multiplayer room has ended. Click to display room results."
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString MultiplayerRoomEnded => new TranslatableString(getKey(@"multiplayer_room_ended"), @"This multiplayer room has ended. Click to display room results.");
|
||||||
|
|
||||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ namespace osu.Game.Online.API
|
|||||||
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
|
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
|
||||||
|
|
||||||
private readonly Bindable<UserStatus> configStatus = new Bindable<UserStatus>();
|
private readonly Bindable<UserStatus> configStatus = new Bindable<UserStatus>();
|
||||||
|
private readonly Bindable<bool> configSupporter = new Bindable<bool>();
|
||||||
|
|
||||||
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
|
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
|
||||||
private readonly Logger log;
|
private readonly Logger log;
|
||||||
|
|
||||||
@@ -104,6 +106,7 @@ namespace osu.Game.Online.API
|
|||||||
authentication.Token.ValueChanged += onTokenChanged;
|
authentication.Token.ValueChanged += onTokenChanged;
|
||||||
|
|
||||||
config.BindWith(OsuSetting.UserOnlineStatus, configStatus);
|
config.BindWith(OsuSetting.UserOnlineStatus, configStatus);
|
||||||
|
config.BindWith(OsuSetting.WasSupporter, configSupporter);
|
||||||
|
|
||||||
if (HasLogin)
|
if (HasLogin)
|
||||||
{
|
{
|
||||||
@@ -333,6 +336,7 @@ namespace osu.Game.Online.API
|
|||||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||||
|
|
||||||
localUser.Value = me;
|
localUser.Value = me;
|
||||||
|
configSupporter.Value = me.IsSupporter;
|
||||||
state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth;
|
state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth;
|
||||||
failureCount = 0;
|
failureCount = 0;
|
||||||
};
|
};
|
||||||
@@ -368,7 +372,8 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
localUser.Value = new APIUser
|
localUser.Value = new APIUser
|
||||||
{
|
{
|
||||||
Username = ProvidedUsername
|
Username = ProvidedUsername,
|
||||||
|
IsSupporter = configSupporter.Value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,6 +612,7 @@ namespace osu.Game.Online.API
|
|||||||
Schedule(() =>
|
Schedule(() =>
|
||||||
{
|
{
|
||||||
localUser.Value = createGuestUser();
|
localUser.Value = createGuestUser();
|
||||||
|
configSupporter.Value = false;
|
||||||
friends.Clear();
|
friends.Clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,18 @@ using osu.Game.Rulesets;
|
|||||||
using osu.Game.Screens.Select.Leaderboards;
|
using osu.Game.Screens.Select.Leaderboards;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using System.Text;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.IO.Network;
|
||||||
|
using osu.Game.Extensions;
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests
|
namespace osu.Game.Online.API.Requests
|
||||||
{
|
{
|
||||||
public class GetScoresRequest : APIRequest<APIScoresCollection>, IEquatable<GetScoresRequest>
|
public class GetScoresRequest : APIRequest<APIScoresCollection>, IEquatable<GetScoresRequest>
|
||||||
{
|
{
|
||||||
public const int MAX_SCORES_PER_REQUEST = 50;
|
public const int DEFAULT_SCORES_PER_REQUEST = 50;
|
||||||
|
public const int MAX_SCORES_PER_REQUEST = 100;
|
||||||
|
|
||||||
private readonly IBeatmapInfo beatmapInfo;
|
private readonly IBeatmapInfo beatmapInfo;
|
||||||
private readonly BeatmapLeaderboardScope scope;
|
private readonly BeatmapLeaderboardScope scope;
|
||||||
@@ -36,19 +39,20 @@ namespace osu.Game.Online.API.Requests
|
|||||||
this.mods = mods ?? Array.Empty<IMod>();
|
this.mods = mods ?? Array.Empty<IMod>();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string Target => $@"beatmaps/{beatmapInfo.OnlineID}/scores{createQueryParameters()}";
|
protected override string Target => $@"beatmaps/{beatmapInfo.OnlineID}/scores";
|
||||||
|
|
||||||
private string createQueryParameters()
|
protected override WebRequest CreateWebRequest()
|
||||||
{
|
{
|
||||||
StringBuilder query = new StringBuilder(@"?");
|
var req = base.CreateWebRequest();
|
||||||
|
|
||||||
query.Append($@"type={scope.ToString().ToLowerInvariant()}");
|
req.AddParameter(@"type", scope.ToString().ToLowerInvariant());
|
||||||
query.Append($@"&mode={ruleset.ShortName}");
|
req.AddParameter(@"mode", ruleset.ShortName);
|
||||||
|
|
||||||
foreach (var mod in mods)
|
foreach (var mod in mods)
|
||||||
query.Append($@"&mods[]={mod.Acronym}");
|
req.AddParameter(@"mods[]", mod.Acronym);
|
||||||
|
|
||||||
return query.ToString();
|
req.AddParameter(@"limit", (scope.RequiresSupporter(mods.Any()) ? MAX_SCORES_PER_REQUEST : DEFAULT_SCORES_PER_REQUEST).ToString(CultureInfo.InvariantCulture));
|
||||||
|
return req;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(GetScoresRequest? other)
|
public bool Equals(GetScoresRequest? other)
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ namespace osu.Game.Online.Chat
|
|||||||
|
|
||||||
switch (args[0])
|
switch (args[0])
|
||||||
{
|
{
|
||||||
case "http":
|
case @"http":
|
||||||
case "https":
|
case @"https":
|
||||||
// length > 3 since all these links need another argument to work
|
// length > 3 since all these links need another argument to work
|
||||||
if (args.Length > 3 && args[1].EndsWith(WebsiteRootUrl, StringComparison.OrdinalIgnoreCase))
|
if (args.Length > 3 && args[1].EndsWith(WebsiteRootUrl, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -139,8 +139,8 @@ namespace osu.Game.Online.Chat
|
|||||||
switch (args[2])
|
switch (args[2])
|
||||||
{
|
{
|
||||||
// old site only
|
// old site only
|
||||||
case "b":
|
case @"b":
|
||||||
case "beatmaps":
|
case @"beatmaps":
|
||||||
{
|
{
|
||||||
string trimmed = mainArg.Split('?').First();
|
string trimmed = mainArg.Split('?').First();
|
||||||
if (int.TryParse(trimmed, out int id))
|
if (int.TryParse(trimmed, out int id))
|
||||||
@@ -149,11 +149,11 @@ namespace osu.Game.Online.Chat
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "s":
|
case @"s":
|
||||||
case "beatmapsets":
|
case @"beatmapsets":
|
||||||
case "d":
|
case @"d":
|
||||||
{
|
{
|
||||||
if (mainArg == "discussions")
|
if (mainArg == @"discussions")
|
||||||
// handle discussion links externally for now
|
// handle discussion links externally for now
|
||||||
return new LinkDetails(LinkAction.External, url);
|
return new LinkDetails(LinkAction.External, url);
|
||||||
|
|
||||||
@@ -169,15 +169,15 @@ namespace osu.Game.Online.Chat
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "u":
|
case @"u":
|
||||||
case "users":
|
case @"users":
|
||||||
return getUserLink(mainArg);
|
return getUserLink(mainArg);
|
||||||
|
|
||||||
case "wiki":
|
case @"wiki":
|
||||||
return new LinkDetails(LinkAction.OpenWiki, string.Join('/', args.Skip(3)));
|
return new LinkDetails(LinkAction.OpenWiki, string.Join('/', args.Skip(3)));
|
||||||
|
|
||||||
case "home":
|
case @"home":
|
||||||
if (mainArg != "changelog")
|
if (mainArg != @"changelog")
|
||||||
// handle link other than changelog as external for now
|
// handle link other than changelog as external for now
|
||||||
return new LinkDetails(LinkAction.External, url);
|
return new LinkDetails(LinkAction.External, url);
|
||||||
|
|
||||||
@@ -192,13 +192,26 @@ namespace osu.Game.Online.Chat
|
|||||||
return new LinkDetails(LinkAction.OpenChangelog, $"{args[4]}/{args[5]}");
|
return new LinkDetails(LinkAction.OpenChangelog, $"{args[4]}/{args[5]}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case @"multiplayer":
|
||||||
|
if (mainArg != @"rooms")
|
||||||
|
return new LinkDetails(LinkAction.External, url);
|
||||||
|
|
||||||
|
if (args.Length == 5)
|
||||||
|
{
|
||||||
|
// https://osu.ppy.sh/multiplayer/rooms/{id}
|
||||||
|
// route used for both multiplayer and playlists
|
||||||
|
return new LinkDetails(LinkAction.JoinRoom, args[4]);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "osu":
|
case @"osu":
|
||||||
// every internal link also needs some kind of argument
|
// every internal link also needs some kind of argument
|
||||||
if (args.Length < 3)
|
if (args.Length < 3)
|
||||||
break;
|
break;
|
||||||
@@ -207,38 +220,39 @@ namespace osu.Game.Online.Chat
|
|||||||
|
|
||||||
switch (args[1])
|
switch (args[1])
|
||||||
{
|
{
|
||||||
case "chan":
|
case @"chan":
|
||||||
linkType = LinkAction.OpenChannel;
|
linkType = LinkAction.OpenChannel;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "edit":
|
case @"edit":
|
||||||
linkType = LinkAction.OpenEditorTimestamp;
|
linkType = LinkAction.OpenEditorTimestamp;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "b":
|
case @"b":
|
||||||
linkType = LinkAction.OpenBeatmap;
|
linkType = LinkAction.OpenBeatmap;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "s":
|
case @"s":
|
||||||
case "dl":
|
case @"dl":
|
||||||
linkType = LinkAction.OpenBeatmapSet;
|
linkType = LinkAction.OpenBeatmapSet;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "spectate":
|
case @"spectate":
|
||||||
linkType = LinkAction.Spectate;
|
linkType = LinkAction.Spectate;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "u":
|
case @"u":
|
||||||
return getUserLink(args[2]);
|
return getUserLink(args[2]);
|
||||||
|
|
||||||
|
case @"room":
|
||||||
|
linkType = LinkAction.JoinRoom;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return new LinkDetails(LinkAction.External, url);
|
return new LinkDetails(LinkAction.External, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LinkDetails(linkType, HttpUtility.UrlDecode(args[2]));
|
return new LinkDetails(linkType, HttpUtility.UrlDecode(args[2]));
|
||||||
|
|
||||||
case "osump":
|
|
||||||
return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LinkDetails(LinkAction.External, url);
|
return new LinkDetails(LinkAction.External, url);
|
||||||
@@ -337,7 +351,7 @@ namespace osu.Game.Online.Chat
|
|||||||
OpenBeatmapSet,
|
OpenBeatmapSet,
|
||||||
OpenChannel,
|
OpenChannel,
|
||||||
OpenEditorTimestamp,
|
OpenEditorTimestamp,
|
||||||
JoinMultiplayerMatch,
|
JoinRoom,
|
||||||
Spectate,
|
Spectate,
|
||||||
OpenUserProfile,
|
OpenUserProfile,
|
||||||
SearchBeatmapSet,
|
SearchBeatmapSet,
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ using osu.Game.Screens.Footer;
|
|||||||
using osu.Game.Screens.Menu;
|
using osu.Game.Screens.Menu;
|
||||||
using osu.Game.Screens.OnlinePlay.DailyChallenge;
|
using osu.Game.Screens.OnlinePlay.DailyChallenge;
|
||||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
@@ -79,6 +80,7 @@ using osu.Game.Utils;
|
|||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using Sentry;
|
using Sentry;
|
||||||
|
using MatchType = osu.Game.Online.Rooms.MatchType;
|
||||||
|
|
||||||
namespace osu.Game
|
namespace osu.Game
|
||||||
{
|
{
|
||||||
@@ -186,7 +188,7 @@ namespace osu.Game
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the back button is currently displayed.
|
/// Whether the back button is currently displayed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IBindable<bool> backButtonVisibility = new Bindable<bool>();
|
private readonly IBindable<bool> backButtonVisibility = new BindableBool();
|
||||||
|
|
||||||
IBindable<LocalUserPlayingState> ILocalUserPlayInfo.PlayingState => UserPlayingState;
|
IBindable<LocalUserPlayingState> ILocalUserPlayInfo.PlayingState => UserPlayingState;
|
||||||
|
|
||||||
@@ -491,7 +493,6 @@ namespace osu.Game
|
|||||||
HandleTimestamp(argString);
|
HandleTimestamp(argString);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case LinkAction.JoinMultiplayerMatch:
|
|
||||||
case LinkAction.Spectate:
|
case LinkAction.Spectate:
|
||||||
waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification
|
waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification
|
||||||
{
|
{
|
||||||
@@ -523,6 +524,11 @@ namespace osu.Game
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case LinkAction.JoinRoom:
|
||||||
|
if (long.TryParse(argString, out long roomId))
|
||||||
|
JoinRoom(roomId);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action.");
|
throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action.");
|
||||||
}
|
}
|
||||||
@@ -598,6 +604,28 @@ namespace osu.Game
|
|||||||
/// <param name="version">The build version of the update stream</param>
|
/// <param name="version">The build version of the update stream</param>
|
||||||
public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version));
|
public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Joins a multiplayer or playlists room with the given <paramref name="id"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void JoinRoom(long id)
|
||||||
|
{
|
||||||
|
var request = new GetRoomRequest(id);
|
||||||
|
request.Success += room =>
|
||||||
|
{
|
||||||
|
switch (room.Type)
|
||||||
|
{
|
||||||
|
case MatchType.Playlists:
|
||||||
|
PresentPlaylist(room);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
PresentMultiplayerMatch(room, string.Empty);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
API.Queue(request);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Seeks to the provided <paramref name="timestamp"/> if the editor is currently open.
|
/// Seeks to the provided <paramref name="timestamp"/> if the editor is currently open.
|
||||||
/// Can also select objects as indicated by the <paramref name="timestamp"/> (depends on ruleset implementation).
|
/// Can also select objects as indicated by the <paramref name="timestamp"/> (depends on ruleset implementation).
|
||||||
@@ -725,6 +753,22 @@ namespace osu.Game
|
|||||||
/// <param name="password">The password to join the room, if any is given.</param>
|
/// <param name="password">The password to join the room, if any is given.</param>
|
||||||
public void PresentMultiplayerMatch(Room room, string password)
|
public void PresentMultiplayerMatch(Room room, string password)
|
||||||
{
|
{
|
||||||
|
if (room.HasEnded)
|
||||||
|
{
|
||||||
|
// TODO: Eventually it should be possible to display ended multiplayer rooms in game too,
|
||||||
|
// but it generally will require turning off the entirety of communication with spectator server which is currently embedded into multiplayer screens.
|
||||||
|
Notifications.Post(new SimpleNotification
|
||||||
|
{
|
||||||
|
Text = NotificationsStrings.MultiplayerRoomEnded,
|
||||||
|
Activated = () =>
|
||||||
|
{
|
||||||
|
OpenUrlExternally($@"/multiplayer/rooms/{room.RoomID}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
PerformFromScreen(screen =>
|
PerformFromScreen(screen =>
|
||||||
{
|
{
|
||||||
if (!(screen is Multiplayer multiplayer))
|
if (!(screen is Multiplayer multiplayer))
|
||||||
@@ -736,6 +780,23 @@ namespace osu.Game
|
|||||||
// but `PerformFromScreen` doesn't understand nested stacks.
|
// but `PerformFromScreen` doesn't understand nested stacks.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Join a playlist immediately.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="room">The playlist to join.</param>
|
||||||
|
public void PresentPlaylist(Room room)
|
||||||
|
{
|
||||||
|
PerformFromScreen(screen =>
|
||||||
|
{
|
||||||
|
if (!(screen is Playlists playlists))
|
||||||
|
screen.Push(playlists = new Playlists());
|
||||||
|
|
||||||
|
playlists.Join(room);
|
||||||
|
});
|
||||||
|
// TODO: We should really be able to use `validScreens: new[] { typeof(Playlists) }` here
|
||||||
|
// but `PerformFromScreen` doesn't understand nested stacks.
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Present a score's replay immediately.
|
/// Present a score's replay immediately.
|
||||||
/// The user should have already requested this interactively.
|
/// The user should have already requested this interactively.
|
||||||
@@ -1044,7 +1105,7 @@ namespace osu.Game
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.BottomLeft,
|
Anchor = Anchor.BottomLeft,
|
||||||
Origin = Anchor.BottomLeft,
|
Origin = Anchor.BottomLeft,
|
||||||
Action = () => ScreenFooter.OnBack?.Invoke(),
|
Action = handleBackButton,
|
||||||
},
|
},
|
||||||
logoContainer = new Container { RelativeSizeAxes = Axes.Both },
|
logoContainer = new Container { RelativeSizeAxes = Axes.Both },
|
||||||
footerBasedOverlayContent = new Container
|
footerBasedOverlayContent = new Container
|
||||||
@@ -1059,14 +1120,7 @@ namespace osu.Game
|
|||||||
Child = ScreenFooter = new ScreenFooter(backReceptor)
|
Child = ScreenFooter = new ScreenFooter(backReceptor)
|
||||||
{
|
{
|
||||||
RequestLogoInFront = inFront => ScreenContainer.ChangeChildDepth(logoContainer, inFront ? float.MinValue : 0),
|
RequestLogoInFront = inFront => ScreenContainer.ChangeChildDepth(logoContainer, inFront ? float.MinValue : 0),
|
||||||
OnBack = () =>
|
BackButtonPressed = handleBackButton
|
||||||
{
|
|
||||||
if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowUserExit && !currentScreen.OnBackButton()))
|
|
||||||
ScreenStack.Exit();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1247,6 +1301,13 @@ namespace osu.Game
|
|||||||
handleStartupImport();
|
handleStartupImport();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleBackButton()
|
||||||
|
{
|
||||||
|
if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen)) return;
|
||||||
|
|
||||||
|
if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowUserExit && !currentScreen.OnBackButton())) ScreenStack.Exit();
|
||||||
|
}
|
||||||
|
|
||||||
private void handleStartupImport()
|
private void handleStartupImport()
|
||||||
{
|
{
|
||||||
if (args?.Length > 0)
|
if (args?.Length > 0)
|
||||||
@@ -1657,7 +1718,6 @@ namespace osu.Game
|
|||||||
// Bind to new screen.
|
// Bind to new screen.
|
||||||
if (newScreen != null)
|
if (newScreen != null)
|
||||||
{
|
{
|
||||||
backButtonVisibility.BindTo(newScreen.BackButtonVisibility);
|
|
||||||
OverlayActivationMode.BindTo(newScreen.OverlayActivationMode);
|
OverlayActivationMode.BindTo(newScreen.OverlayActivationMode);
|
||||||
configUserActivity.BindTo(newScreen.Activity);
|
configUserActivity.BindTo(newScreen.Activity);
|
||||||
|
|
||||||
@@ -1669,19 +1729,36 @@ namespace osu.Game
|
|||||||
else
|
else
|
||||||
Toolbar.Show();
|
Toolbar.Show();
|
||||||
|
|
||||||
|
var newOsuScreen = (OsuScreen)newScreen;
|
||||||
|
|
||||||
if (newScreen.ShowFooter)
|
if (newScreen.ShowFooter)
|
||||||
{
|
{
|
||||||
|
// the legacy back button should never display while the new footer is in use, as it
|
||||||
|
// contains its own local back button.
|
||||||
|
((BindableBool)backButtonVisibility).Value = false;
|
||||||
|
|
||||||
BackButton.Hide();
|
BackButton.Hide();
|
||||||
ScreenFooter.SetButtons(newScreen.CreateFooterButtons());
|
|
||||||
ScreenFooter.Show();
|
ScreenFooter.Show();
|
||||||
|
|
||||||
|
newOsuScreen.OnLoadComplete += _ =>
|
||||||
|
{
|
||||||
|
var buttons = newScreen.CreateFooterButtons();
|
||||||
|
|
||||||
|
newOsuScreen.LoadComponentsAgainstScreenDependencies(buttons);
|
||||||
|
|
||||||
|
ScreenFooter.SetButtons(buttons);
|
||||||
|
ScreenFooter.Show();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
backButtonVisibility.BindTo(newScreen.BackButtonVisibility);
|
||||||
|
|
||||||
ScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
|
ScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
|
||||||
ScreenFooter.Hide();
|
ScreenFooter.Hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
skinEditor.SetTarget((OsuScreen)newScreen);
|
skinEditor.SetTarget(newOsuScreen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.ComponentModel;
|
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Resources.Localisation.Web;
|
using osu.Game.Resources.Localisation.Web;
|
||||||
|
|
||||||
@@ -9,10 +8,10 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
{
|
{
|
||||||
public enum MetadataType
|
public enum MetadataType
|
||||||
{
|
{
|
||||||
[Description("User Tags")] // TODO: use translated string after osu-resources update
|
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoUserTags))]
|
||||||
UserTags,
|
UserTags,
|
||||||
|
|
||||||
[Description("Mapper Tags")] // TODO: use translated string after osu-resources update
|
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoMapperTags))]
|
||||||
MapperTags,
|
MapperTags,
|
||||||
|
|
||||||
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoSource))]
|
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoSource))]
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Profile.Sections
|
|||||||
|
|
||||||
CurrentPage = CurrentPage?.TakeNext(ItemsPerPage) ?? new PaginationParameters(InitialItemsCount);
|
CurrentPage = CurrentPage?.TakeNext(ItemsPerPage) ?? new PaginationParameters(InitialItemsCount);
|
||||||
|
|
||||||
retrievalRequest = CreateRequest(User.Value, CurrentPage.Value);
|
retrievalRequest = CreateRequest(User.Value, new PaginationParameters(CurrentPage.Value.Offset, CurrentPage.Value.Limit + 1));
|
||||||
retrievalRequest.Success += items => UpdateItems(items, loadCancellation);
|
retrievalRequest.Success += items => UpdateItems(items, loadCancellation);
|
||||||
|
|
||||||
api.Queue(retrievalRequest);
|
api.Queue(retrievalRequest);
|
||||||
@@ -124,8 +124,6 @@ namespace osu.Game.Overlays.Profile.Sections
|
|||||||
|
|
||||||
protected virtual void UpdateItems(List<TModel> items, CancellationTokenSource cancellationTokenSource) => Schedule(() =>
|
protected virtual void UpdateItems(List<TModel> items, CancellationTokenSource cancellationTokenSource) => Schedule(() =>
|
||||||
{
|
{
|
||||||
OnItemsReceived(items);
|
|
||||||
|
|
||||||
if (!items.Any() && CurrentPage?.Offset == 0)
|
if (!items.Any() && CurrentPage?.Offset == 0)
|
||||||
{
|
{
|
||||||
moreButton.Hide();
|
moreButton.Hide();
|
||||||
@@ -137,11 +135,18 @@ namespace osu.Game.Overlays.Profile.Sections
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool hasMore = items.Count > CurrentPage?.Limit;
|
||||||
|
|
||||||
|
if (hasMore)
|
||||||
|
items.RemoveAt(items.Count - 1);
|
||||||
|
|
||||||
|
OnItemsReceived(items);
|
||||||
|
|
||||||
LoadComponentsAsync(items.Select(CreateDrawableItem).Where(d => d != null).Cast<Drawable>(), drawables =>
|
LoadComponentsAsync(items.Select(CreateDrawableItem).Where(d => d != null).Cast<Drawable>(), drawables =>
|
||||||
{
|
{
|
||||||
missing.Hide();
|
missing.Hide();
|
||||||
|
|
||||||
moreButton.FadeTo(items.Count == CurrentPage?.Limit ? 1 : 0);
|
moreButton.FadeTo(hasMore ? 1 : 0);
|
||||||
moreButton.IsLoading = false;
|
moreButton.IsLoading = false;
|
||||||
|
|
||||||
ItemsContainer.AddRange(drawables);
|
ItemsContainer.AddRange(drawables);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
@@ -31,6 +32,18 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
|
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
|
||||||
public virtual BindableBool AdjustPitch { get; } = new BindableBool();
|
public virtual BindableBool AdjustPitch { get; } = new BindableBool();
|
||||||
|
|
||||||
|
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
foreach (var description in base.SettingDescription)
|
||||||
|
yield return description;
|
||||||
|
|
||||||
|
if (!AdjustPitch.IsDefault)
|
||||||
|
yield return ("Adjust pitch", AdjustPitch.Value ? "On" : "Off");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private readonly RateAdjustModHelper rateAdjustHelper;
|
private readonly RateAdjustModHelper rateAdjustHelper;
|
||||||
|
|
||||||
protected ModDoubleTime()
|
protected ModDoubleTime()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
@@ -31,6 +32,18 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
|
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
|
||||||
public virtual BindableBool AdjustPitch { get; } = new BindableBool();
|
public virtual BindableBool AdjustPitch { get; } = new BindableBool();
|
||||||
|
|
||||||
|
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
foreach (var description in base.SettingDescription)
|
||||||
|
yield return description;
|
||||||
|
|
||||||
|
if (!AdjustPitch.IsDefault)
|
||||||
|
yield return ("Adjust pitch", AdjustPitch.Value ? "On" : "Off");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private readonly RateAdjustModHelper rateAdjustHelper;
|
private readonly RateAdjustModHelper rateAdjustHelper;
|
||||||
|
|
||||||
protected ModHalfTime()
|
protected ModHalfTime()
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
yield return ("Speed change", $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x");
|
yield return ("Speed change", $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x");
|
||||||
|
|
||||||
|
if (!AdjustPitch.IsDefault)
|
||||||
|
yield return ("Adjust pitch", AdjustPitch.Value ? "On" : "Off");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,6 +206,9 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// <returns>One half of the hit window for <paramref name="result"/>.</returns>
|
/// <returns>One half of the hit window for <paramref name="result"/>.</returns>
|
||||||
public double WindowFor(HitResult result)
|
public double WindowFor(HitResult result)
|
||||||
{
|
{
|
||||||
|
if (!IsHitResultAllowed(result))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(result), result, $@"{result} is not an allowed result.");
|
||||||
|
|
||||||
switch (result)
|
switch (result)
|
||||||
{
|
{
|
||||||
case HitResult.Perfect:
|
case HitResult.Perfect:
|
||||||
@@ -227,7 +230,7 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
return miss;
|
return miss;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new ArgumentException("Unknown enum member", nameof(result));
|
throw new ArgumentOutOfRangeException(nameof(result), result, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,17 +256,7 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
new DifficultyRange(HitResult.Miss, 0, 0, 0),
|
new DifficultyRange(HitResult.Miss, 0, 0, 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
public override bool IsHitResultAllowed(HitResult result)
|
public override bool IsHitResultAllowed(HitResult result) => true;
|
||||||
{
|
|
||||||
switch (result)
|
|
||||||
{
|
|
||||||
case HitResult.Perfect:
|
|
||||||
case HitResult.Miss:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override DifficultyRange[] GetRanges() => ranges;
|
protected override DifficultyRange[] GetRanges() => ranges;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.Effects;
|
using osu.Framework.Graphics.Effects;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
@@ -101,36 +102,38 @@ namespace osu.Game.Rulesets.UI
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mod? displayedContent;
|
private (LocalisableString setting, LocalisableString value)[]? displayedSettings;
|
||||||
|
|
||||||
public void SetContent(Mod content)
|
public void SetContent(Mod content)
|
||||||
{
|
{
|
||||||
if (content == displayedContent)
|
|
||||||
return;
|
|
||||||
|
|
||||||
displayedContent = content;
|
|
||||||
nameText.Text = content.Name;
|
nameText.Text = content.Name;
|
||||||
settingsLabelsFlow.Clear();
|
|
||||||
settingsValuesFlow.Clear();
|
|
||||||
|
|
||||||
if (content.SettingDescription.Any())
|
if (displayedSettings == null || !displayedSettings.SequenceEqual(content.SettingDescription))
|
||||||
{
|
{
|
||||||
settingsLabelsFlow.Show();
|
displayedSettings = content.SettingDescription.ToArray();
|
||||||
settingsValuesFlow.Show();
|
|
||||||
|
|
||||||
foreach (var part in content.SettingDescription)
|
settingsLabelsFlow.Clear();
|
||||||
|
settingsValuesFlow.Clear();
|
||||||
|
|
||||||
|
if (displayedSettings.Any())
|
||||||
{
|
{
|
||||||
settingsLabelsFlow.AddText(part.setting);
|
settingsLabelsFlow.Show();
|
||||||
settingsLabelsFlow.NewLine();
|
settingsValuesFlow.Show();
|
||||||
|
|
||||||
settingsValuesFlow.AddText(part.value);
|
foreach (var part in displayedSettings)
|
||||||
settingsValuesFlow.NewLine();
|
{
|
||||||
|
settingsLabelsFlow.AddText(part.setting);
|
||||||
|
settingsLabelsFlow.NewLine();
|
||||||
|
|
||||||
|
settingsValuesFlow.AddText(part.value);
|
||||||
|
settingsValuesFlow.NewLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
settingsLabelsFlow.Hide();
|
||||||
|
settingsValuesFlow.Hide();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
settingsLabelsFlow.Hide();
|
|
||||||
settingsValuesFlow.Hide();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ namespace osu.Game.Rulesets.UI
|
|||||||
this.target = target;
|
this.target = target;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
Depth = float.MinValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
private IBeatmap currentBeatmap;
|
private IBeatmap currentBeatmap;
|
||||||
private Ruleset currentRuleset;
|
private Ruleset currentRuleset;
|
||||||
|
|
||||||
private float beatmapOffset;
|
private long beatmapOffset;
|
||||||
|
|
||||||
public Score Parse(Stream stream)
|
public Score Parse(Stream stream)
|
||||||
{
|
{
|
||||||
@@ -262,7 +262,7 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
|
|
||||||
private void readLegacyReplay(Replay replay, StreamReader reader)
|
private void readLegacyReplay(Replay replay, StreamReader reader)
|
||||||
{
|
{
|
||||||
float lastTime = beatmapOffset;
|
long lastTime = beatmapOffset;
|
||||||
var legacyFrames = new List<LegacyReplayFrame>();
|
var legacyFrames = new List<LegacyReplayFrame>();
|
||||||
|
|
||||||
string[] frames = reader.ReadToEnd().Split(',');
|
string[] frames = reader.ReadToEnd().Split(',');
|
||||||
@@ -283,7 +283,7 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
// In mania, mouseX encodes the pressed keys in the lower 20 bits
|
// In mania, mouseX encodes the pressed keys in the lower 20 bits
|
||||||
int mouseXParseLimit = currentRuleset.RulesetInfo.OnlineID == 3 ? (1 << 20) - 1 : Parsing.MAX_COORDINATE_VALUE;
|
int mouseXParseLimit = currentRuleset.RulesetInfo.OnlineID == 3 ? (1 << 20) - 1 : Parsing.MAX_COORDINATE_VALUE;
|
||||||
|
|
||||||
float diff = Parsing.ParseFloat(split[0]);
|
int diff = Parsing.ParseInt(split[0]);
|
||||||
float mouseX = Parsing.ParseFloat(split[1], mouseXParseLimit);
|
float mouseX = Parsing.ParseFloat(split[1], mouseXParseLimit);
|
||||||
float mouseY = Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE);
|
float mouseY = Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE);
|
||||||
|
|
||||||
|
|||||||
@@ -108,12 +108,14 @@ namespace osu.Game.Screens.Backgrounds
|
|||||||
if (Background != null)
|
if (Background != null)
|
||||||
{
|
{
|
||||||
newDepth = Background.Depth + 1;
|
newDepth = Background.Depth + 1;
|
||||||
Background.FinishTransforms();
|
|
||||||
Background.FadeOut(250);
|
Background.FadeOut(250);
|
||||||
Background.Expire();
|
Background.Expire();
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Depth = newDepth;
|
b.Depth = newDepth;
|
||||||
|
b.Anchor = b.Origin = Anchor.Centre;
|
||||||
|
b.FadeInFromZero(500, Easing.OutQuint);
|
||||||
|
b.ScaleTo(1.02f).ScaleTo(1, 3500, Easing.OutQuint);
|
||||||
dimmable.Background = Background = b;
|
dimmable.Background = Background = b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ namespace osu.Game.Screens.Backgrounds
|
|||||||
|
|
||||||
if (videoFiles.Length == 0)
|
if (videoFiles.Length == 0)
|
||||||
{
|
{
|
||||||
Stream videoName = textures.GetStream("Menu/default_video.webm");
|
Stream videoName = textures.GetStream("EzResources/default_video.webm");
|
||||||
return new StreamVideoBackgroundScreen(videoName);
|
return new StreamVideoBackgroundScreen(videoName);
|
||||||
// return new Background(getBackgroundTextureName());
|
// return new Background(getBackgroundTextureName());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ namespace osu.Game.Screens.Edit.Submission
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (playableBeatmap.BeatmapInfo.OnlineID > 0)
|
if (playableBeatmap.BeatmapInfo.OnlineID > 0)
|
||||||
throw new InvalidOperationException(@"Encountered beatmap with ID that has not been assigned to it by the server!");
|
throw new InvalidOperationException($@"Difficulty ""{playableBeatmap.BeatmapInfo.DifficultyName}"" has BeatmapID {playableBeatmap.BeatmapInfo.OnlineID} that has not been assigned to it by the server!");
|
||||||
|
|
||||||
if (allocatedBeatmapIds.Count == 0)
|
if (allocatedBeatmapIds.Count == 0)
|
||||||
throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!");
|
throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!");
|
||||||
|
|||||||
@@ -23,12 +23,24 @@ namespace osu.Game.Screens.Footer
|
|||||||
{
|
{
|
||||||
public partial class ScreenFooter : OverlayContainer
|
public partial class ScreenFooter : OverlayContainer
|
||||||
{
|
{
|
||||||
|
public ScreenBackButton BackButton { get; private set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when logo tracking begins, intended to bring the osu! logo to the frontmost visually.
|
||||||
|
/// </summary>
|
||||||
|
public Action<bool>? RequestLogoInFront { private get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The back button was pressed.
|
||||||
|
/// </summary>
|
||||||
|
public Action? BackButtonPressed { private get; init; }
|
||||||
|
|
||||||
|
public const int HEIGHT = 50;
|
||||||
|
|
||||||
private const int padding = 60;
|
private const int padding = 60;
|
||||||
private const float delay_per_button = 30;
|
private const float delay_per_button = 30;
|
||||||
private const double transition_duration = 400;
|
private const double transition_duration = 400;
|
||||||
|
|
||||||
public const int HEIGHT = 50;
|
|
||||||
|
|
||||||
private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
|
private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
|
||||||
|
|
||||||
private Box background = null!;
|
private Box background = null!;
|
||||||
@@ -40,15 +52,6 @@ namespace osu.Game.Screens.Footer
|
|||||||
[Cached]
|
[Cached]
|
||||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private OsuGame? game { get; set; }
|
|
||||||
|
|
||||||
public ScreenBackButton BackButton { get; private set; } = null!;
|
|
||||||
|
|
||||||
public Action<bool>? RequestLogoInFront { get; set; }
|
|
||||||
|
|
||||||
public Action? OnBack;
|
|
||||||
|
|
||||||
public ScreenFooter(BackReceptor? receptor = null)
|
public ScreenFooter(BackReceptor? receptor = null)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
@@ -144,8 +147,7 @@ namespace osu.Game.Screens.Footer
|
|||||||
{
|
{
|
||||||
logoTrackingContainer.StopTracking();
|
logoTrackingContainer.StopTracking();
|
||||||
|
|
||||||
if (game != null)
|
changeLogoDepthDelegate = Scheduler.AddDelayed(() => RequestLogoInFront?.Invoke(false), transition_duration);
|
||||||
changeLogoDepthDelegate = Scheduler.AddDelayed(() => RequestLogoInFront?.Invoke(false), transition_duration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PopIn()
|
protected override void PopIn()
|
||||||
@@ -326,7 +328,7 @@ namespace osu.Game.Screens.Footer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
OnBack?.Invoke();
|
BackButtonPressed?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class BackReceptor : Drawable, IKeyBindingHandler<GlobalAction>
|
public partial class BackReceptor : Drawable, IKeyBindingHandler<GlobalAction>
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ namespace osu.Game.Screens
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the back button should be displayed in this screen.
|
/// Whether the back button should be displayed in this screen.
|
||||||
|
/// Note that this property is ignored when <see cref="ShowFooter"/> is <c>true</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
// todo: make this work with footer.
|
||||||
IBindable<bool> BackButtonVisibility { get; }
|
IBindable<bool> BackButtonVisibility { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -97,7 +99,7 @@ namespace osu.Game.Screens
|
|||||||
Bindable<RulesetInfo> Ruleset { get; }
|
Bindable<RulesetInfo> Ruleset { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of footer buttons to be added to the game footer, or empty to display no buttons.
|
/// Buttons to be added to the game's footer toolbar.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IReadOnlyList<ScreenFooterButton> CreateFooterButtons();
|
IReadOnlyList<ScreenFooterButton> CreateFooterButtons();
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ using osu.Game.Seasonal;
|
|||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
|
using osu.Game.Screens.SelectV2;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Menu
|
namespace osu.Game.Screens.Menu
|
||||||
{
|
{
|
||||||
@@ -239,7 +240,13 @@ namespace osu.Game.Screens.Menu
|
|||||||
|
|
||||||
public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial;
|
public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial;
|
||||||
|
|
||||||
private void loadSoloSongSelect() => this.Push(new PlaySongSelect());
|
private void loadSoloSongSelect()
|
||||||
|
{
|
||||||
|
if (GetContainingInputManager()!.CurrentState.Keyboard.ControlPressed)
|
||||||
|
this.Push(new SoloSongSelect());
|
||||||
|
else
|
||||||
|
this.Push(new PlaySongSelect());
|
||||||
|
}
|
||||||
|
|
||||||
public override void OnEntering(ScreenTransitionEvent e)
|
public override void OnEntering(ScreenTransitionEvent e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
public GameplayChatDisplay(Room room)
|
public GameplayChatDisplay(Room room)
|
||||||
: base(room, leaveChannelOnDispose: false)
|
: base(room, leaveChannelOnDispose: false)
|
||||||
{
|
{
|
||||||
|
RelativeSizeAxes = Axes.X;
|
||||||
Background.Alpha = 0.2f;
|
Background.Alpha = 0.2f;
|
||||||
Width = width;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ using System.Threading.Tasks;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
@@ -18,6 +20,7 @@ using osu.Game.Screens.Play;
|
|||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Screens.Select.Leaderboards;
|
using osu.Game.Screens.Select.Leaderboards;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||||
{
|
{
|
||||||
@@ -25,8 +28,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
{
|
{
|
||||||
protected override bool PauseOnFocusLost => false;
|
protected override bool PauseOnFocusLost => false;
|
||||||
|
|
||||||
protected override bool ShowLeaderboard => true;
|
|
||||||
|
|
||||||
protected override UserActivity InitialActivity => new UserActivity.InMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
|
protected override UserActivity InitialActivity => new UserActivity.InMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
@@ -42,6 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
private readonly MultiplayerLeaderboardProvider leaderboardProvider;
|
private readonly MultiplayerLeaderboardProvider leaderboardProvider;
|
||||||
|
|
||||||
private GameplayMatchScoreDisplay teamScoreDisplay = null!;
|
private GameplayMatchScoreDisplay teamScoreDisplay = null!;
|
||||||
|
private GameplayChatDisplay chat = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Construct a multiplayer player.
|
/// Construct a multiplayer player.
|
||||||
@@ -57,7 +59,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
AllowFailAnimation = false,
|
AllowFailAnimation = false,
|
||||||
AllowSkipping = room.AutoSkip,
|
AllowSkipping = room.AutoSkip,
|
||||||
AutomaticallySkipIntro = room.AutoSkip,
|
AutomaticallySkipIntro = room.AutoSkip,
|
||||||
AlwaysShowLeaderboard = true,
|
ShowLeaderboard = true,
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
leaderboardProvider = new MultiplayerLeaderboardProvider(users);
|
leaderboardProvider = new MultiplayerLeaderboardProvider(users);
|
||||||
@@ -71,16 +73,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
|
|
||||||
ScoreProcessor.ApplyNewJudgementsWhenFailed = true;
|
ScoreProcessor.ApplyNewJudgementsWhenFailed = true;
|
||||||
|
|
||||||
LoadComponentAsync(new GameplayChatDisplay(Room)
|
LoadComponentAsync(new FillFlowContainer
|
||||||
{
|
{
|
||||||
Expanded = { BindTarget = LeaderboardExpandedState },
|
Width = 260,
|
||||||
}, chat => HUDOverlay.LeaderboardFlow.Insert(2, chat));
|
Direction = FillDirection.Vertical,
|
||||||
|
Spacing = new Vector2(5),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
chat = new GameplayChatDisplay(Room),
|
||||||
|
teamScoreDisplay = new GameplayMatchScoreDisplay
|
||||||
|
{
|
||||||
|
Expanded = { BindTarget = HUDOverlay.ShowHud },
|
||||||
|
Alpha = 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, HUDOverlay.TopLeftElements.Add);
|
||||||
|
LoadComponentAsync(new MultiplayerPositionDisplay
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomRight,
|
||||||
|
Origin = Anchor.BottomRight,
|
||||||
|
}, d => HUDOverlay.BottomRightElements.Insert(-1, d));
|
||||||
|
|
||||||
LoadComponentAsync(teamScoreDisplay = new GameplayMatchScoreDisplay
|
|
||||||
{
|
|
||||||
Expanded = { BindTarget = HUDOverlay.ShowHud },
|
|
||||||
Alpha = 0,
|
|
||||||
}, scoreDisplay => HUDOverlay.LeaderboardFlow.Insert(1, scoreDisplay));
|
|
||||||
LoadComponentAsync(leaderboardProvider, loaded =>
|
LoadComponentAsync(leaderboardProvider, loaded =>
|
||||||
{
|
{
|
||||||
AddInternal(loaded);
|
AddInternal(loaded);
|
||||||
@@ -124,6 +137,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
failAndBail();
|
failAndBail();
|
||||||
}
|
}
|
||||||
}), true);
|
}), true);
|
||||||
|
|
||||||
|
LocalUserPlaying.BindValueChanged(_ => chat.Expanded.Value = !LocalUserPlaying.Value, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user