同步更新,增加可选EZ风判定图,优化EZ候选列表

This commit is contained in:
LA
2025-05-17 12:20:56 +08:00
parent c7720aad14
commit 7946303c52
150 changed files with 5668 additions and 1387 deletions

View File

View File

@@ -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.

View File

@@ -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.

View File

@@ -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" ]
] }
} }
}

View 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));
}
}
}

View File

@@ -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(),
} }
}; };
} }

View File

@@ -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)

View 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));
}
}
}

View File

@@ -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,

View File

@@ -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
{ {

View File

@@ -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;

View File

@@ -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
{ {

View File

@@ -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();
}
}
}
}

View File

@@ -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;
} }

View File

@@ -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",

View File

@@ -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();
}
} }
} }

View File

@@ -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
{ {
} }
} }

View File

@@ -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;

View File

@@ -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(),

View File

@@ -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(),
}; };
} }

View File

@@ -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();

View 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));
}
}
}

View File

@@ -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);

View File

@@ -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);
} }

View File

@@ -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)

View File

@@ -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();

View File

@@ -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(),
} }
}; };
} }

View File

@@ -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
});
}
}

View 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));
}
}
}

View 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));
}
}
}

View File

@@ -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);

View File

@@ -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)

View File

@@ -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);
}
}
}

View File

@@ -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);
}
} }
} }

View File

@@ -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:

View File

@@ -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>

View File

@@ -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;
} }
} }
} }

View File

@@ -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")]

View File

@@ -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

View File

@@ -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]

View File

@@ -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 }
} }
}); });
}); });

View File

@@ -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();
} }

View File

@@ -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);
}); });
} }

View File

@@ -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));
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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());

View File

@@ -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)]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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>();
}
}
}

View File

@@ -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;
}
}
} }
} }

View 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>());
}
}
}
}

View File

@@ -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]

View File

@@ -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);
}
} }
} }

View File

@@ -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);
}
} }
} }

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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));
}
} }
} }

View File

@@ -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", () =>

View File

@@ -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"));
}
} }
} }

View File

@@ -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)
{
}
}
} }
} }

View File

@@ -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 },
}, },

View File

@@ -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>());
}
}
} }
} }

View File

@@ -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));
}
}

View File

@@ -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);
});
}
}
}

View 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]);
}
}
}

View File

@@ -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());
} }

View File

@@ -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());
}
} }
} }

View File

@@ -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",
}, },
} }
}, },

View File

@@ -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;

View File

@@ -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,
} }
} }

View File

@@ -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
} }
} }

View File

@@ -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)

View File

@@ -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);
} }
} }

View File

@@ -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);

View File

@@ -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;

View File

@@ -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,

View File

@@ -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}";
} }
} }

View File

@@ -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();
}); });

View File

@@ -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)

View File

@@ -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,

View File

@@ -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);
} }
} }

View File

@@ -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))]

View File

@@ -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);

View File

@@ -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()

View File

@@ -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()

View File

@@ -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");
} }
} }

View File

@@ -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;
} }

View File

@@ -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();
} }
} }

View File

@@ -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()

View File

@@ -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);

View File

@@ -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;
} }

View File

@@ -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());
} }

View File

@@ -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!");

View File

@@ -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>

View File

@@ -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();

View File

@@ -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)
{ {

View File

@@ -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]

View File

@@ -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