mirror of
https://github.com/SK-la/Ez2Lazer.git
synced 2026-03-13 11:20:28 +00:00
同步更新,增加可选EZ风判定图,优化EZ候选列表
This commit is contained in:
@@ -25,7 +25,3 @@ M:TagLib.File.Create(System.String);TagLib's MIME type detection changes behavio
|
||||
M:TagLib.File.Create(TagLib.File.IFileAbstraction);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||
M:TagLib.File.Create(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(System.String);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||
M:TagLib.File.Create(TagLib.File.IFileAbstraction);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||
M:TagLib.File.Create(System.String,TagLib.ReadStyle);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||
M:TagLib.File.Create(TagLib.File.IFileAbstraction,TagLib.ReadStyle);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.419.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.512.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
{
|
||||
"solution": {
|
||||
"path": "osu.sln",
|
||||
"projects": [
|
||||
"..\\osu-resources\\osu.Game.Resources\\osu.Game.Resources.csproj",
|
||||
"osu.Desktop\\osu.Desktop.csproj",
|
||||
"osu.Game.Benchmarks\\osu.Game.Benchmarks.csproj",
|
||||
"osu.Game.Rulesets.Catch.Tests\\osu.Game.Rulesets.Catch.Tests.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\\osu.Game.Rulesets.Mania.csproj",
|
||||
"osu.Game.Rulesets.Osu.Tests\\osu.Game.Rulesets.Osu.Tests.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\\osu.Game.Rulesets.Taiko.csproj",
|
||||
"osu.Game.Tests\\osu.Game.Tests.csproj",
|
||||
"osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj",
|
||||
"osu.Game.Tournament\\osu.Game.Tournament.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\\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\\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\\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\\osu.Game.Rulesets.Pippidon.csproj",
|
||||
"../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj"
|
||||
]
|
||||
}
|
||||
}
|
||||
"solution": {
|
||||
"path": "osu.sln",
|
||||
"projects": [
|
||||
"..\\osu-resources\\osu.Game.Resources\\osu.Game.Resources.csproj",
|
||||
"osu.Desktop\\osu.Desktop.csproj",
|
||||
"osu.Game.Benchmarks\\osu.Game.Benchmarks.csproj",
|
||||
"osu.Game.Rulesets.Catch.Tests\\osu.Game.Rulesets.Catch.Tests.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\\osu.Game.Rulesets.Mania.csproj",
|
||||
"osu.Game.Rulesets.Osu.Tests\\osu.Game.Rulesets.Osu.Tests.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\\osu.Game.Rulesets.Taiko.csproj",
|
||||
"osu.Game.Tests\\osu.Game.Tests.csproj",
|
||||
"osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj",
|
||||
"osu.Game.Tournament\\osu.Game.Tournament.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\\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\\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\\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\\osu.Game.Rulesets.Pippidon.csproj"
|
||||
]
|
||||
}
|
||||
}
|
||||
75
osu.Game.Rulesets.Catch.Tests/TestSceneReplayRecording.cs
Normal file
75
osu.Game.Rulesets.Catch.Tests/TestSceneReplayRecording.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public partial class TestSceneReplayRecording : PlayerTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; } = null!;
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Fruit { StartTime = 0, },
|
||||
new Fruit { StartTime = 5000, },
|
||||
new Fruit { StartTime = 10000, },
|
||||
new Fruit { StartTime = 15000, }
|
||||
}
|
||||
};
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
|
||||
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||
|
||||
[Test]
|
||||
public void TestRecording()
|
||||
{
|
||||
seekTo(0);
|
||||
AddStep("start moving left", () => InputManager.PressKey(Key.Left));
|
||||
seekTo(5000);
|
||||
AddStep("end moving left", () => InputManager.ReleaseKey(Key.Left));
|
||||
AddAssert("catcher max left", () => this.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(0));
|
||||
AddAssert("movement to left recorded to replay", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => f.Actions.SequenceEqual([CatchAction.MoveLeft])));
|
||||
AddAssert("replay reached left edge", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => Precision.AlmostEquals(f.Position, 0)));
|
||||
|
||||
AddStep("start dashing right", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.PressKey(Key.Right);
|
||||
});
|
||||
seekTo(10000);
|
||||
AddStep("end dashing right", () =>
|
||||
{
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
InputManager.ReleaseKey(Key.Right);
|
||||
});
|
||||
AddAssert("catcher max right", () => this.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(CatchPlayfield.WIDTH));
|
||||
AddAssert("dash to right recorded to replay", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => f.Actions.SequenceEqual([CatchAction.Dash, CatchAction.MoveRight])));
|
||||
AddAssert("replay reached right edge", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => Precision.AlmostEquals(f.Position, CatchPlayfield.WIDTH)));
|
||||
}
|
||||
|
||||
private void seekTo(double time)
|
||||
{
|
||||
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
|
||||
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
|
||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||
|
||||
if (keyCounter != null)
|
||||
{
|
||||
@@ -64,12 +65,20 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
spectatorList.Origin = Anchor.BottomLeft;
|
||||
spectatorList.Position = new Vector2(10, -10);
|
||||
}
|
||||
|
||||
if (leaderboard != null)
|
||||
{
|
||||
leaderboard.Anchor = Anchor.CentreLeft;
|
||||
leaderboard.Origin = Anchor.CentreLeft;
|
||||
leaderboard.X = 10;
|
||||
}
|
||||
})
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new LegacyKeyCounterDisplay(),
|
||||
new SpectatorList(),
|
||||
new DrawableGameplayLeaderboard(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,9 +31,12 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss);
|
||||
|
||||
[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),
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
@@ -47,6 +50,32 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
},
|
||||
},
|
||||
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(2000)
|
||||
|
||||
59
osu.Game.Rulesets.Mania.Tests/TestSceneReplayRecording.cs
Normal file
59
osu.Game.Rulesets.Mania.Tests/TestSceneReplayRecording.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public partial class TestSceneReplayRecording : PlayerTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; } = null!;
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new ManiaBeatmap(new StageDefinition(1))
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Note { StartTime = 0, },
|
||||
new Note { StartTime = 5000, },
|
||||
new Note { StartTime = 10000, },
|
||||
new Note { StartTime = 15000, }
|
||||
},
|
||||
Difficulty = { CircleSize = 1 },
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = ruleset,
|
||||
}
|
||||
};
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
|
||||
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||
|
||||
[Test]
|
||||
public void TestRecording()
|
||||
{
|
||||
seekTo(0);
|
||||
AddStep("press space", () => InputManager.Key(Key.Space));
|
||||
AddAssert("button press recorded to replay", () => Player.Score.Replay.Frames.OfType<ManiaReplayFrame>().Any(f => f.Actions.SequenceEqual([ManiaAction.Key1])));
|
||||
}
|
||||
|
||||
private void seekTo(double time)
|
||||
{
|
||||
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
|
||||
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
SetDefault(ManiaRulesetSetting.ScrollStyle, ManiaScrollingStyle.ScrollTimeStyleFixed);
|
||||
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
|
||||
SetDefault(ManiaRulesetSetting.ScrollPerKeyMode, false);
|
||||
SetDefault(ManiaRulesetSetting.PerspectiveAngle, 90.0f, 30.0f, 90.0f);
|
||||
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, true);
|
||||
SetDefault(ManiaRulesetSetting.MobileLayout, ManiaMobileLayout.Portrait);
|
||||
|
||||
@@ -72,6 +73,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
ScrollStyle,
|
||||
// HitMode,
|
||||
|
||||
PerspectiveAngle,
|
||||
ColumnWidth,
|
||||
SpecialFactor,
|
||||
ScrollPerKeyMode,
|
||||
|
||||
@@ -2,7 +2,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
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
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// 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 osu.Framework.Bindables;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@@ -9,13 +11,16 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModPerfect : ModPerfect
|
||||
{
|
||||
[SettingSource("Require perfect hits")]
|
||||
public BindableBool RequirePerfectHits { get; } = new BindableBool();
|
||||
|
||||
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
|
||||
{
|
||||
if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type))
|
||||
return false;
|
||||
|
||||
// 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 != result.Judgement.MaxResult;
|
||||
|
||||
@@ -40,9 +40,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
case GlobalSkinnableContainers.MainHUDComponents:
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
|
||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||
|
||||
if (leaderboard != null)
|
||||
leaderboard.Position = new Vector2(36, 115);
|
||||
|
||||
if (combo != null)
|
||||
{
|
||||
combo.ShowLabel.Value = false;
|
||||
@@ -55,6 +59,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
spectatorList.Position = new Vector2(36, -66);
|
||||
})
|
||||
{
|
||||
new DrawableGameplayLeaderboard(),
|
||||
new ArgonManiaComboCounter(),
|
||||
new SpectatorList
|
||||
{
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
{
|
||||
public partial class EzColumnHitErrorMeter : HitErrorMeter
|
||||
{
|
||||
[SettingSource("Icon Height", "Icon Height")]
|
||||
public BindableNumber<float> IconHeight { get; } = new BindableNumber<float>(2)
|
||||
{
|
||||
MinValue = 1,
|
||||
MaxValue = 20,
|
||||
Precision = 1f,
|
||||
};
|
||||
|
||||
[SettingSource("Move Height", "Move Height")]
|
||||
public BindableNumber<float> MoveHeight { get; } = new BindableNumber<float>(10)
|
||||
{
|
||||
MinValue = 1,
|
||||
MaxValue = 50,
|
||||
Precision = 1f,
|
||||
};
|
||||
|
||||
[SettingSource("Background Alpha", "Background Alpha")]
|
||||
public BindableNumber<float> BackgroundAlpha { get; } = new BindableNumber<float>(0.2f)
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 1,
|
||||
Precision = 0.1f,
|
||||
};
|
||||
|
||||
private double[] floatingAverages = null!;
|
||||
private Box[] judgementMarkers = null!;
|
||||
private Container[] columns = null!;
|
||||
private float keyCount;
|
||||
|
||||
private OsuConfigManager config = null!;
|
||||
|
||||
[Resolved]
|
||||
private InputCountController controller { get; set; } = null!;
|
||||
|
||||
public EzColumnHitErrorMeter()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
this.config = config;
|
||||
recreateComponents();
|
||||
}
|
||||
|
||||
private void recreateComponents()
|
||||
{
|
||||
keyCount = controller.Triggers.Count;
|
||||
floatingAverages = new double[(int)keyCount];
|
||||
judgementMarkers = new Box[(int)keyCount];
|
||||
|
||||
InternalChild = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding(2),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(0, 0),
|
||||
Children = columns = Enumerable.Range(0, (int)keyCount).Select(index =>
|
||||
{
|
||||
var column = createColumn();
|
||||
var marker = new Box
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativePositionAxes = Axes.Y,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Width = (float)config.Get<double>(OsuSetting.ColumnWidth),
|
||||
Height = IconHeight.Value,
|
||||
Colour = Colour4.Gray,
|
||||
Alpha = 0.8f
|
||||
};
|
||||
column.Add(marker);
|
||||
judgementMarkers[index] = marker;
|
||||
return column;
|
||||
}).ToArray()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
controller.Triggers.BindCollectionChanged((_, __) => recreateComponents(), true);
|
||||
|
||||
// 更新标识块高度
|
||||
IconHeight.BindValueChanged(height =>
|
||||
{
|
||||
foreach (var marker in judgementMarkers)
|
||||
marker.Height = height.NewValue;
|
||||
}, true);
|
||||
|
||||
// 更新背景柱状列高度和标识块移动范围
|
||||
MoveHeight.BindValueChanged(height =>
|
||||
{
|
||||
foreach (var column in columns)
|
||||
{
|
||||
var backgroundBox = column.Children.OfType<Box>().FirstOrDefault();
|
||||
|
||||
if (backgroundBox != null)
|
||||
{
|
||||
backgroundBox.Height = height.NewValue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var marker in judgementMarkers)
|
||||
{
|
||||
marker.Y = Math.Clamp(marker.Y, -height.NewValue / 2, height.NewValue / 2);
|
||||
// 重新计算标识块的相对位置
|
||||
float currentAbsoluteY = marker.Y * height.OldValue;
|
||||
float newRelativeY = currentAbsoluteY / height.NewValue;
|
||||
|
||||
marker.Y = newRelativeY;
|
||||
|
||||
// 更新标识块的移动范围
|
||||
marker.MoveToY(newRelativeY, 800, Easing.OutQuint);
|
||||
}
|
||||
|
||||
Invalidate(Invalidation.DrawSize);
|
||||
}, true);
|
||||
|
||||
// 更新背景透明度
|
||||
BackgroundAlpha.BindValueChanged(alpha =>
|
||||
{
|
||||
foreach (var column in columns)
|
||||
{
|
||||
var backgroundBox = column.Children.OfType<Box>().FirstOrDefault();
|
||||
if (backgroundBox != null)
|
||||
backgroundBox.Alpha = alpha.NewValue;
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
private Container createColumn()
|
||||
{
|
||||
var backgroundBox = new Box
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Height = MoveHeight.Value,
|
||||
Width = (float)config.Get<double>(OsuSetting.ColumnWidth),
|
||||
Colour = Colour4.Gray,
|
||||
Alpha = 0.2f
|
||||
};
|
||||
|
||||
return new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
backgroundBox
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnNewJudgement(JudgementResult judgement)
|
||||
{
|
||||
if (!judgement.IsHit || !judgement.Type.IsScorable())
|
||||
return;
|
||||
|
||||
int columnIndex = getColumnIndex(judgement);
|
||||
if (columnIndex < 0 || columnIndex >= keyCount)
|
||||
return;
|
||||
|
||||
floatingAverages[columnIndex] = floatingAverages[columnIndex] * 0.9 + judgement.TimeOffset * 0.1;
|
||||
|
||||
const int marker_move_duration = 800;
|
||||
var marker = judgementMarkers[columnIndex];
|
||||
|
||||
float targetY = getRelativeJudgementPosition(floatingAverages[columnIndex]);
|
||||
|
||||
marker.Y = targetY;
|
||||
|
||||
marker.MoveToY(targetY, marker_move_duration, Easing.OutQuint);
|
||||
|
||||
marker.Colour = GetColourForHitResult(judgement.Type);
|
||||
}
|
||||
|
||||
private float getRelativeJudgementPosition(double value)
|
||||
{
|
||||
float moveRange = MoveHeight.Value;
|
||||
return (float)Math.Clamp((value / HitWindows.WindowFor(HitResult.Miss)) * moveRange, -moveRange, moveRange);
|
||||
}
|
||||
|
||||
private int getColumnIndex(JudgementResult judgement)
|
||||
{
|
||||
if (judgement.HitObject is IHasColumn hasColumn)
|
||||
return hasColumn.Column;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
foreach (var column in columns)
|
||||
{
|
||||
column.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
{
|
||||
public partial class EzComComboCounter : ComboCounter
|
||||
{
|
||||
[SettingSource("Font", "Font", SettingControlType = typeof(OffsetNumberNameSelector))]
|
||||
public Bindable<OffsetNumberName> NameDropdown { get; } = new Bindable<OffsetNumberName>(OffsetNumberName.EZ2DJ_4th);
|
||||
[SettingSource("Font", "Font", SettingControlType = typeof(EzEnumListSelector))]
|
||||
public Bindable<OffsetNumberName> NameDropdown { get; } = new Bindable<OffsetNumberName>((OffsetNumberName)4);
|
||||
|
||||
[SettingSource("Effect Type", "Effect Type")]
|
||||
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))]
|
||||
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
|
||||
|
||||
public EzCounterText Text = null!;
|
||||
public EzComboText Text = null!;
|
||||
protected override double RollingDuration => 250;
|
||||
protected virtual bool DisplayXSymbol => true;
|
||||
|
||||
@@ -142,9 +142,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
|
||||
protected override IHasText CreateText()
|
||||
{
|
||||
Text = new EzCounterText(NameDropdown)
|
||||
Text = new EzComboText(NameDropdown)
|
||||
{
|
||||
Scale = new Vector2(2.2f),
|
||||
Scale = new Vector2(1.8f),
|
||||
};
|
||||
return Text;
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
{
|
||||
public partial class EzComComboSprite : HitErrorMeter
|
||||
{
|
||||
[SettingSource("Combo Text Font", "Combo Text Font", SettingControlType = typeof(OffsetNumberNameSelector))]
|
||||
public Bindable<OffsetNumberName> NameDropdown { get; } = new Bindable<OffsetNumberName>(OffsetNumberName.EZ2DJ_4th);
|
||||
[SettingSource("Combo Text Font", "Combo Text Font", SettingControlType = typeof(EzEnumListSelector))]
|
||||
public Bindable<OffsetNumberName> NameDropdown { get; } = new Bindable<OffsetNumberName>((OffsetNumberName)4);
|
||||
|
||||
[SettingSource("Effect Type", "Effect Type")]
|
||||
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))]
|
||||
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
|
||||
|
||||
public EzCounterText Text = null!;
|
||||
public EzComboText Text = null!;
|
||||
|
||||
public Bindable<int> Current { get; } = new Bindable<int>();
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
Text = new EzCounterText(NameDropdown)
|
||||
Text = new EzComboText(NameDropdown)
|
||||
{
|
||||
Scale = new Vector2(0.8f),
|
||||
Text = "c",
|
||||
|
||||
@@ -11,10 +11,10 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD.JudgementCounter;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Skinning.Components;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@@ -32,14 +32,28 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
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 bool isDragging;
|
||||
public Sprite? StaticSprite;
|
||||
private Container? fullComboSprite;
|
||||
private Sample? fullComboSound;
|
||||
|
||||
[Resolved]
|
||||
private TextureStore textures { get; set; } = null!;
|
||||
|
||||
protected HitWindows HitWindows { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScoreProcessor processor { get; set; } = null!;
|
||||
|
||||
@@ -59,19 +73,27 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
// private (HitResult result, double length)[] hitWindows = null!;
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
private Sprite? fullComboSprite;
|
||||
private Sample? fullComboSound;
|
||||
fullComboSprite = new Container
|
||||
{
|
||||
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()
|
||||
{
|
||||
@@ -81,19 +103,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
|
||||
processor.NewJudgement += processorNewJudgement;
|
||||
|
||||
// 加载 FULL COMBO 贴图和音效
|
||||
fullComboSprite = new Sprite
|
||||
NameDropdown.BindValueChanged(_ =>
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(1.5f),
|
||||
// Y = -20,
|
||||
Alpha = 0, // 初始隐藏
|
||||
Texture = textures.Get("Gameplay/AllCombo/ALL-COMBO2"), // 替换为你的贴图路径
|
||||
};
|
||||
AddInternal(fullComboSprite);
|
||||
ClearInternal(true);
|
||||
|
||||
fullComboSound = sampleStore.Get("Gameplay/full_combo_sound"); // 替换为你的音效路径
|
||||
StaticSprite?.Invalidate();
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void processorNewJudgement(JudgementResult j)
|
||||
@@ -115,12 +130,116 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
if (!judgement.Type.IsScorable() || judgement.Type.IsBonus())
|
||||
return;
|
||||
|
||||
ClearInternal(true);
|
||||
// 清除内部元素前先结束所有变换
|
||||
StaticSprite?.FinishTransforms();
|
||||
|
||||
ClearInternal();
|
||||
StaticSprite = null;
|
||||
|
||||
var judgementText = CreateJudgementTexture(judgement.Type);
|
||||
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()
|
||||
{
|
||||
var missCounter = judgementCountController.Counters
|
||||
@@ -129,13 +248,132 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
if (missCounter.ResultCount.Value == 0 && fullComboSprite != null)
|
||||
{
|
||||
// 显示 FULL COMBO 贴图
|
||||
fullComboSprite.FadeIn(200).Then().FadeOut(5000);
|
||||
fullComboSprite.Alpha = 1;
|
||||
fullComboSprite.FadeIn(50).Then().FadeOut(5000);
|
||||
|
||||
// 播放音效
|
||||
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()
|
||||
{
|
||||
FinishTransforms(true);
|
||||
@@ -151,43 +389,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
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)
|
||||
{
|
||||
dragStartPosition = e.ScreenSpaceMousePosition;
|
||||
@@ -209,70 +410,5 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
{
|
||||
isDragging = false;
|
||||
}
|
||||
|
||||
private string getGifPath(HitResult hitResult)
|
||||
{
|
||||
return hitResult switch
|
||||
{
|
||||
HitResult.Miss => @"Gameplay/Ez2/score/Miss",
|
||||
HitResult.Meh => @"Gameplay/Ez2/score/Fail1",
|
||||
HitResult.Ok => @"Gameplay/Ez2/score/Fail",
|
||||
HitResult.Good => @"Gameplay/Ez2/score/Good",
|
||||
HitResult.Great => @"Gameplay/Ez2/score/Cool",
|
||||
HitResult.Perfect => @"Gameplay/Ez2/score/Kool",
|
||||
_ => @"Gameplay/Ez2/score",
|
||||
};
|
||||
}
|
||||
|
||||
public virtual void PlayAnimation(HitResult hitResult, Drawable drawable)
|
||||
{
|
||||
const float flash_speed = 60f;
|
||||
|
||||
switch (hitResult)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
applyFadeEffect(drawable, new[] { Color4.Red, Color4.IndianRed }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Meh:
|
||||
applyFadeEffect(drawable, new[] { Color4.Purple, Color4.MediumPurple }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Ok:
|
||||
applyFadeEffect(drawable, new[] { Color4.ForestGreen, Color4.SeaGreen }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Good:
|
||||
applyFadeEffect(drawable, new[] { Color4.Green, Color4.LightGreen }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Great:
|
||||
applyFadeEffect(drawable, new[] { Color4.AliceBlue, Color4.LightSkyBlue }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Perfect:
|
||||
applyFadeEffect(drawable, new[] { Color4.LightBlue, Color4.LightGreen }, flash_speed);
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void applyFadeEffect(Drawable drawable, Color4[] colors, double flashSpeed)
|
||||
{
|
||||
if (!drawable.IsLoaded)
|
||||
return;
|
||||
|
||||
drawable.FadeColour(colors[0], 0);
|
||||
var sequence = drawable.FadeColour(colors[0], flashSpeed, Easing.OutQuint);
|
||||
|
||||
for (int i = 1; i < colors.Length; i++)
|
||||
{
|
||||
sequence = sequence.Then().FadeColour(colors[i], flashSpeed, Easing.OutQuint);
|
||||
}
|
||||
|
||||
// sequence.Loop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
[Cached]
|
||||
public partial class EzComHitTiming : HitErrorMeter
|
||||
{
|
||||
[SettingSource("Offset Number Font", "Offset Number Font", SettingControlType = typeof(OffsetNumberNameSelector))]
|
||||
public Bindable<OffsetNumberName> NumberNameDropdown { get; } = new Bindable<OffsetNumberName>(OffsetNumberName.Tomato);
|
||||
[SettingSource("Offset Number Font", "Offset Number Font", SettingControlType = typeof(EzEnumListSelector))]
|
||||
public Bindable<OffsetNumberName> NumberNameDropdown { get; } = new Bindable<OffsetNumberName>((OffsetNumberName)28);
|
||||
|
||||
[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")]
|
||||
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 FillFlowContainer errorContainer = null!;
|
||||
private EzCounterText timingTextL = null!;
|
||||
private EzCounterText timingText = null!;
|
||||
private EzCounterText timingTextR = null!;
|
||||
private EzCounterText offsetText = null!;
|
||||
private EzComboText timingTextL = null!;
|
||||
private EzComboText timingText = null!;
|
||||
private EzComboText timingTextR = null!;
|
||||
private EzComboText offsetText = null!;
|
||||
private Box backgroundBox = null!;
|
||||
|
||||
public EzComHitTiming()
|
||||
@@ -101,11 +101,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(0.8f),
|
||||
Scale = new Vector2(2f),
|
||||
// Spacing = new Vector2(SymmetryOffset.Value),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
timingTextL = new EzCounterText(TextNameDropdown)
|
||||
timingTextL = new EzComboText(TextNameDropdown)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -113,14 +113,14 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
Alpha = 1,
|
||||
Position = new Vector2(-SymmetryOffset.Value, 0)
|
||||
},
|
||||
timingText = new EzCounterText(TextNameDropdown)
|
||||
timingText = new EzComboText(TextNameDropdown)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "e/l",
|
||||
Alpha = 0
|
||||
},
|
||||
timingTextR = new EzCounterText(TextNameDropdown)
|
||||
timingTextR = new EzComboText(TextNameDropdown)
|
||||
{
|
||||
Anchor = 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,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(1.2f),
|
||||
Scale = new Vector2(1.5f),
|
||||
Text = "±000",
|
||||
},
|
||||
}
|
||||
@@ -278,7 +278,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
None,
|
||||
}
|
||||
|
||||
public partial class OffsetTextNameSelector : OffsetNumberNameSelector
|
||||
public partial class OffsetTextNameSelector : EzEnumListSelector
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
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.UI.Scrolling;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
@@ -128,13 +128,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
||||
keyCounter.Position = new Vector2(0, -Stage.HIT_TARGET_POSITION - stage_padding_bottom);
|
||||
}
|
||||
|
||||
var columnHitErrorMeter = container.OfType<EzColumnHitErrorMeter>().FirstOrDefault();
|
||||
var columnHitErrorMeter = container.OfType<EzComHitTimingColumns>().FirstOrDefault();
|
||||
|
||||
if (columnHitErrorMeter != null)
|
||||
{
|
||||
columnHitErrorMeter.Anchor = Anchor.Centre;
|
||||
columnHitErrorMeter.Origin = Anchor.Centre;
|
||||
columnHitErrorMeter.Position = new Vector2(0, 20);
|
||||
columnHitErrorMeter.Y = 100;
|
||||
}
|
||||
|
||||
var hitErrorMeter = container.OfType<BarHitErrorMeter>().FirstOrDefault();
|
||||
@@ -167,7 +167,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
||||
new EzComComboCounter(),
|
||||
new EzComComboCounter(),
|
||||
new EzComKeyCounterDisplay(),
|
||||
new EzColumnHitErrorMeter(),
|
||||
new EzComHitTimingColumns(),
|
||||
new BarHitErrorMeter(),
|
||||
new EzComHitResultScore(),
|
||||
new EzComHitTiming(),
|
||||
|
||||
@@ -98,6 +98,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
|
||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||
|
||||
if (combo != null)
|
||||
{
|
||||
@@ -112,10 +113,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
spectatorList.Origin = Anchor.BottomLeft;
|
||||
spectatorList.Position = new Vector2(10, -10);
|
||||
}
|
||||
|
||||
if (leaderboard != null)
|
||||
{
|
||||
leaderboard.Anchor = Anchor.CentreLeft;
|
||||
leaderboard.Origin = Anchor.CentreLeft;
|
||||
leaderboard.X = 10;
|
||||
}
|
||||
})
|
||||
{
|
||||
new LegacyManiaComboCounter(),
|
||||
new SpectatorList(),
|
||||
new DrawableGameplayLeaderboard(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.SbI
|
||||
combo1.Origin = Anchor.Centre;
|
||||
combo1.Y = 200;
|
||||
combo1.Effect.Value = EffectType.None;
|
||||
combo1.NameDropdown.Value = OffsetNumberName.EZ2AC_EVOLVE;
|
||||
combo1.NameDropdown.Value = (OffsetNumberName)43;
|
||||
}
|
||||
|
||||
var hitErrorMeter = container.OfType<BarHitErrorMeter>().FirstOrDefault();
|
||||
|
||||
81
osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs
Normal file
81
osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneReplayRecording : PlayerTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; } = null!;
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
StartTime = 0,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
StartTime = 5000,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
StartTime = 10000,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
StartTime = 15000,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
|
||||
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||
|
||||
[Test]
|
||||
public void TestRecording()
|
||||
{
|
||||
seekTo(0);
|
||||
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
|
||||
AddStep("press X", () => InputManager.Key(Key.X));
|
||||
AddAssert("right button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.RightButton])));
|
||||
|
||||
seekTo(5000);
|
||||
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
|
||||
AddStep("press Z", () => InputManager.Key(Key.Z));
|
||||
AddAssert("left button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.LeftButton])));
|
||||
|
||||
seekTo(10000);
|
||||
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
|
||||
AddStep("press C", () => InputManager.Key(Key.C));
|
||||
AddAssert("smoke button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.Smoke])));
|
||||
}
|
||||
|
||||
private void seekTo(double time)
|
||||
{
|
||||
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
|
||||
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,6 +275,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
else
|
||||
{
|
||||
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.
|
||||
proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance;
|
||||
proposedDistance = Math.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||
|
||||
@@ -259,6 +259,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||
(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);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonSliderBody : PlaySliderBody
|
||||
{
|
||||
// Eventually this would be a user setting.
|
||||
public float BodyAlpha { get; init; } = 1;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
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 Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour)
|
||||
{
|
||||
return base.GetBodyAccentColour(skin, hitObjectAccentColour).Opacity(BodyAlpha);
|
||||
}
|
||||
|
||||
private partial class DrawableSliderPath : Default.DrawableSliderPath
|
||||
{
|
||||
protected override Color4 ColourAt(float position)
|
||||
|
||||
@@ -16,13 +16,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
|
||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||
{
|
||||
bool isPro = Skin is ArgonProSkin;
|
||||
|
||||
switch (lookup)
|
||||
{
|
||||
case SkinComponentLookup<HitResult> resultComponent:
|
||||
HitResult result = resultComponent.Component;
|
||||
|
||||
// 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();
|
||||
|
||||
switch (result)
|
||||
@@ -46,7 +48,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
return new ArgonMainCirclePiece(false);
|
||||
|
||||
case OsuSkinComponents.SliderBody:
|
||||
return new ArgonSliderBody();
|
||||
return new ArgonSliderBody
|
||||
{
|
||||
BodyAlpha = isPro ? 0.92f : 0.98f
|
||||
};
|
||||
|
||||
case OsuSkinComponents.SliderBall:
|
||||
return new ArgonSliderBall();
|
||||
|
||||
@@ -72,6 +72,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
|
||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||
|
||||
Vector2 pos = new Vector2();
|
||||
|
||||
@@ -89,6 +90,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
spectatorList.Anchor = Anchor.BottomLeft;
|
||||
spectatorList.Origin = Anchor.BottomLeft;
|
||||
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 LegacyKeyCounterDisplay(),
|
||||
new SpectatorList(),
|
||||
new DrawableGameplayLeaderboard(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Replays;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneTaikoModSimplifiedRhythm : TaikoModTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestOneThirdConversion()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSimplifiedRhythm
|
||||
{
|
||||
OneThirdConversion = { Value = true },
|
||||
},
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit { StartTime = 1000, Type = HitType.Centre },
|
||||
new Hit { StartTime = 1500, Type = HitType.Centre },
|
||||
new Hit { StartTime = 2000, Type = HitType.Centre },
|
||||
new Hit { StartTime = 2333, Type = HitType.Rim }, // mod removes this
|
||||
new Hit { StartTime = 2666, Type = HitType.Centre }, // mod moves this to 2500
|
||||
new Hit { StartTime = 3000, Type = HitType.Centre },
|
||||
new Hit { StartTime = 3500, Type = HitType.Centre },
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1200),
|
||||
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1700),
|
||||
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2200),
|
||||
new TaikoReplayFrame(2500, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2700),
|
||||
new TaikoReplayFrame(3000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(3200),
|
||||
new TaikoReplayFrame(3500, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(3700),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 6 && Player.ScoreProcessor.Accuracy.Value == 1
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOneSixthConversion() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSimplifiedRhythm
|
||||
{
|
||||
OneSixthConversion = { Value = true }
|
||||
},
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit { StartTime = 1000, Type = HitType.Centre },
|
||||
new Hit { StartTime = 1250, Type = HitType.Centre },
|
||||
new Hit { StartTime = 1500, Type = HitType.Centre },
|
||||
new Hit { StartTime = 1666, Type = HitType.Rim }, // mod removes this
|
||||
new Hit { StartTime = 1833, Type = HitType.Centre }, // mod moves this to 1750
|
||||
new Hit { StartTime = 2000, Type = HitType.Centre },
|
||||
new Hit { StartTime = 2250, Type = HitType.Centre },
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1200),
|
||||
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1450),
|
||||
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1600),
|
||||
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1800),
|
||||
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2200),
|
||||
new TaikoReplayFrame(2250, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2450),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 6 && Player.ScoreProcessor.Accuracy.Value == 1
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestOneEighthConversion() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSimplifiedRhythm
|
||||
{
|
||||
OneEighthConversion = { Value = true }
|
||||
},
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () =>
|
||||
{
|
||||
const double one_eighth_timing = 125;
|
||||
|
||||
return new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit { StartTime = 1000, Type = HitType.Centre },
|
||||
new Hit { StartTime = 1250, Type = HitType.Centre },
|
||||
new Hit { StartTime = 1500, Type = HitType.Centre },
|
||||
new Hit { StartTime = 1500 + one_eighth_timing * 1, Type = HitType.Rim }, // mod removes this
|
||||
new Hit { StartTime = 1500 + one_eighth_timing * 2 },
|
||||
new Hit { StartTime = 2000, Type = HitType.Centre },
|
||||
new Hit { StartTime = 2000 + one_eighth_timing * 1, Type = HitType.Centre }, // mod removes this
|
||||
new Hit { StartTime = 2000 + one_eighth_timing * 2, Type = HitType.Centre },
|
||||
new Hit { StartTime = 2000 + one_eighth_timing * 3, Type = HitType.Centre }, // mod removes this
|
||||
new Hit { StartTime = 2000 + one_eighth_timing * 4, Type = HitType.Centre },
|
||||
new Hit { StartTime = 2000 + one_eighth_timing * 5, Type = HitType.Centre }, // mod removes this
|
||||
new Hit { StartTime = 2000 + one_eighth_timing * 6, Type = HitType.Centre },
|
||||
new Hit { StartTime = 2000 + one_eighth_timing * 7, Type = HitType.Centre }, // mod removes this
|
||||
},
|
||||
};
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1000),
|
||||
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1250),
|
||||
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1500),
|
||||
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1750),
|
||||
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2000),
|
||||
new TaikoReplayFrame(2250, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2250),
|
||||
new TaikoReplayFrame(2500, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2500),
|
||||
new TaikoReplayFrame(2750, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2750),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 8 && Player.ScoreProcessor.Accuracy.Value == 1
|
||||
});
|
||||
}
|
||||
}
|
||||
65
osu.Game.Rulesets.Taiko.Tests/TestSceneReplayRecording.cs
Normal file
65
osu.Game.Rulesets.Taiko.Tests/TestSceneReplayRecording.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Replays;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
public partial class TestSceneReplayRecording : PlayerTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; } = null!;
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Hit { StartTime = 0, },
|
||||
new Hit { StartTime = 5000, },
|
||||
new Hit { StartTime = 10000, },
|
||||
new Hit { StartTime = 15000, }
|
||||
}
|
||||
};
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
|
||||
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||
|
||||
[Test]
|
||||
public void TestRecording()
|
||||
{
|
||||
seekTo(0);
|
||||
AddStep("press D", () => InputManager.Key(Key.D));
|
||||
AddAssert("left rim press recorded to replay", () => Player.Score.Replay.Frames.OfType<TaikoReplayFrame>().Any(f => f.Actions.SequenceEqual([TaikoAction.LeftRim])));
|
||||
|
||||
seekTo(5000);
|
||||
AddStep("press F", () => InputManager.Key(Key.F));
|
||||
AddAssert("left centre press recorded to replay", () => Player.Score.Replay.Frames.OfType<TaikoReplayFrame>().Any(f => f.Actions.SequenceEqual([TaikoAction.LeftCentre])));
|
||||
|
||||
seekTo(10000);
|
||||
AddStep("press J", () => InputManager.Key(Key.J));
|
||||
AddAssert("right centre press recorded to replay", () => Player.Score.Replay.Frames.OfType<TaikoReplayFrame>().Any(f => f.Actions.SequenceEqual([TaikoAction.RightCentre])));
|
||||
|
||||
seekTo(10000);
|
||||
AddStep("press K", () => InputManager.Key(Key.K));
|
||||
AddAssert("right rim press recorded to replay", () => Player.Score.Replay.Frames.OfType<TaikoReplayFrame>().Any(f => f.Actions.SequenceEqual([TaikoAction.RightRim])));
|
||||
}
|
||||
|
||||
private void seekTo(double time)
|
||||
{
|
||||
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
|
||||
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
|
||||
}
|
||||
}
|
||||
}
|
||||
130
osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs
Normal file
130
osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public class TaikoModSimplifiedRhythm : Mod, IApplicableToBeatmap
|
||||
{
|
||||
public override string Name => "Simplified Rhythm";
|
||||
public override string Acronym => "SR";
|
||||
public override double ScoreMultiplier => 0.6;
|
||||
public override LocalisableString Description => "Simplify tricky rhythms!";
|
||||
public override ModType Type => ModType.DifficultyReduction;
|
||||
|
||||
[SettingSource("1/3 to 1/2 conversion", "Converts 1/3 patterns to 1/2 rhythm.")]
|
||||
public Bindable<bool> OneThirdConversion { get; } = new BindableBool();
|
||||
|
||||
[SettingSource("1/6 to 1/4 conversion", "Converts 1/6 patterns to 1/4 rhythm.")]
|
||||
public Bindable<bool> OneSixthConversion { get; } = new BindableBool(true);
|
||||
|
||||
[SettingSource("1/8 to 1/4 conversion", "Converts 1/8 patterns to 1/4 rhythm.")]
|
||||
public Bindable<bool> OneEighthConversion { get; } = new BindableBool();
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
var taikoBeatmap = (TaikoBeatmap)beatmap;
|
||||
var controlPointInfo = taikoBeatmap.ControlPointInfo;
|
||||
|
||||
Hit[] hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast<Hit>().ToArray();
|
||||
|
||||
if (hits.Length == 0)
|
||||
return;
|
||||
|
||||
var conversions = new List<(int, int)>();
|
||||
|
||||
if (OneEighthConversion.Value) conversions.Add((8, 4));
|
||||
if (OneSixthConversion.Value) conversions.Add((6, 4));
|
||||
if (OneThirdConversion.Value) conversions.Add((3, 2));
|
||||
|
||||
bool inPattern = false;
|
||||
|
||||
foreach ((int baseRhythm, int adjustedRhythm) in conversions)
|
||||
{
|
||||
int patternStartIndex = 0;
|
||||
|
||||
for (int i = 1; i < hits.Length; i++)
|
||||
{
|
||||
double snapValue = getSnapBetweenNotes(controlPointInfo, hits[i - 1], hits[i]);
|
||||
|
||||
if (inPattern)
|
||||
{
|
||||
// pattern continues
|
||||
if (snapValue == baseRhythm) continue;
|
||||
|
||||
inPattern = false;
|
||||
|
||||
processPattern(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (snapValue == baseRhythm)
|
||||
{
|
||||
patternStartIndex = i - 1;
|
||||
inPattern = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process the last pattern if we reached the end of the beatmap and are still in a pattern.
|
||||
if (inPattern)
|
||||
processPattern(hits.Length);
|
||||
|
||||
void processPattern(int patternEndIndex)
|
||||
{
|
||||
// Iterate through the pattern
|
||||
for (int j = patternStartIndex; j < patternEndIndex; j++)
|
||||
{
|
||||
int indexInPattern = j - patternStartIndex;
|
||||
|
||||
switch (baseRhythm)
|
||||
{
|
||||
// 1/8: Remove every second note
|
||||
case 8:
|
||||
{
|
||||
if (indexInPattern % 2 == 1)
|
||||
{
|
||||
taikoBeatmap.HitObjects.Remove(hits[j]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// 1/6 and 1/3: Remove every second note and adjust time of every third
|
||||
case 6:
|
||||
case 3:
|
||||
{
|
||||
if (indexInPattern % 3 == 1)
|
||||
taikoBeatmap.HitObjects.Remove(hits[j]);
|
||||
else if (indexInPattern % 3 == 2)
|
||||
hits[j].StartTime = hits[j + 1].StartTime - controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / adjustedRhythm;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(baseRhythm));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getSnapBetweenNotes(ControlPointInfo controlPointInfo, Hit currentNote, Hit nextNote)
|
||||
{
|
||||
var currentTimingPoint = controlPointInfo.TimingPointAt(currentNote.StartTime);
|
||||
return controlPointInfo.GetClosestBeatDivisor(currentTimingPoint.Time + (nextNote.StartTime - currentNote.StartTime));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
|
||||
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);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// 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.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
{
|
||||
@@ -18,6 +21,59 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
{
|
||||
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:
|
||||
// This should eventually be moved to a skin setting, when supported.
|
||||
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Default
|
||||
{
|
||||
public class TaikoTrianglesSkinTransformer : SkinTransformer
|
||||
{
|
||||
public TaikoTrianglesSkinTransformer(ISkin skin)
|
||||
: base(skin)
|
||||
{
|
||||
}
|
||||
|
||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case GlobalSkinnableContainerLookup containerLookup:
|
||||
{
|
||||
// Only handle per ruleset defaults here.
|
||||
if (containerLookup.Ruleset == null)
|
||||
return base.GetDrawableComponent(lookup);
|
||||
|
||||
switch (containerLookup.Lookup)
|
||||
{
|
||||
case GlobalSkinnableContainers.MainHUDComponents:
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||
|
||||
if (leaderboard != null)
|
||||
{
|
||||
leaderboard.Position = new Vector2(40, -100);
|
||||
leaderboard.Height = 180;
|
||||
leaderboard.Anchor = Anchor.BottomLeft;
|
||||
leaderboard.Origin = Anchor.BottomLeft;
|
||||
}
|
||||
|
||||
if (spectatorList != null)
|
||||
{
|
||||
spectatorList.HeaderFont.Value = Typeface.Venera;
|
||||
spectatorList.HeaderColour.Value = new OsuColour().BlueLighter;
|
||||
spectatorList.Anchor = Anchor.BottomLeft;
|
||||
spectatorList.Origin = Anchor.TopLeft;
|
||||
spectatorList.Position = new Vector2(320, -280);
|
||||
}
|
||||
})
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DrawableGameplayLeaderboard(),
|
||||
new SpectatorList
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return base.GetDrawableComponent(lookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,15 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
@@ -29,119 +32,180 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
|
||||
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
|
||||
if (hasExplosion.Value)
|
||||
return Drawable.Empty().With(d => d.Expire());
|
||||
}
|
||||
|
||||
if (lookup is TaikoSkinComponentLookup taikoComponent)
|
||||
{
|
||||
switch (taikoComponent.Component)
|
||||
case GlobalSkinnableContainerLookup containerLookup:
|
||||
{
|
||||
case TaikoSkinComponents.DrumRollBody:
|
||||
if (GetTexture("taiko-roll-middle") != null)
|
||||
return new LegacyDrumRoll();
|
||||
// Modifications for global components.
|
||||
if (containerLookup.Ruleset == null)
|
||||
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;
|
||||
|
||||
case TaikoSkinComponents.InputDrum:
|
||||
if (hasBarLeft)
|
||||
return new LegacyInputDrum();
|
||||
switch (containerLookup.Lookup)
|
||||
{
|
||||
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:
|
||||
return null;
|
||||
if (combo != null)
|
||||
{
|
||||
combo.Anchor = Anchor.BottomLeft;
|
||||
combo.Origin = Anchor.BottomLeft;
|
||||
combo.Scale = new Vector2(1.28f);
|
||||
|
||||
case TaikoSkinComponents.CentreHit:
|
||||
case TaikoSkinComponents.RimHit:
|
||||
if (hasHitCircle)
|
||||
return new LegacyHit(taikoComponent.Component);
|
||||
pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X);
|
||||
}
|
||||
|
||||
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:
|
||||
return this.GetAnimation("sliderscorepoint", false, false);
|
||||
if (spectatorList != null)
|
||||
{
|
||||
spectatorList.Anchor = Anchor.BottomLeft;
|
||||
spectatorList.Origin = Anchor.TopLeft;
|
||||
spectatorList.Position = pos;
|
||||
}
|
||||
})
|
||||
{
|
||||
new LegacyDefaultComboCounter(),
|
||||
new SpectatorList(),
|
||||
new DrawableGameplayLeaderboard(),
|
||||
};
|
||||
}
|
||||
|
||||
case TaikoSkinComponents.Swell:
|
||||
if (GetTexture("spinner-circle") != null)
|
||||
return new LegacySwell();
|
||||
return null;
|
||||
}
|
||||
|
||||
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:
|
||||
if (GetTexture("taikobigcircle") != null)
|
||||
return new TaikoLegacyHitTarget();
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
case TaikoSkinComponentLookup taikoComponent:
|
||||
{
|
||||
switch (taikoComponent.Component)
|
||||
{
|
||||
case TaikoSkinComponents.DrumRollBody:
|
||||
if (GetTexture("taiko-roll-middle") != null)
|
||||
return new LegacyDrumRoll();
|
||||
|
||||
case TaikoSkinComponents.PlayfieldBackgroundRight:
|
||||
if (GetTexture("taiko-bar-right") != null)
|
||||
return new TaikoLegacyPlayfieldBackgroundRight();
|
||||
return null;
|
||||
|
||||
return null;
|
||||
case TaikoSkinComponents.InputDrum:
|
||||
if (hasBarLeft)
|
||||
return new LegacyInputDrum();
|
||||
|
||||
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();
|
||||
return null;
|
||||
|
||||
return null;
|
||||
case TaikoSkinComponents.DrumSamplePlayer:
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.BarLine:
|
||||
if (GetTexture("taiko-barline") != null)
|
||||
return new LegacyBarLine();
|
||||
case TaikoSkinComponents.CentreHit:
|
||||
case TaikoSkinComponents.RimHit:
|
||||
if (hasHitCircle)
|
||||
return new LegacyHit(taikoComponent.Component);
|
||||
|
||||
return null;
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.TaikoExplosionMiss:
|
||||
var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
|
||||
if (missSprite != null)
|
||||
return new LegacyHitExplosion(missSprite);
|
||||
case TaikoSkinComponents.DrumRollTick:
|
||||
return this.GetAnimation("sliderscorepoint", false, false);
|
||||
|
||||
return null;
|
||||
case TaikoSkinComponents.Swell:
|
||||
if (GetTexture("spinner-circle") != null)
|
||||
return new LegacySwell();
|
||||
|
||||
case TaikoSkinComponents.TaikoExplosionOk:
|
||||
case TaikoSkinComponents.TaikoExplosionGreat:
|
||||
string hitName = getHitName(taikoComponent.Component);
|
||||
var hitSprite = this.GetAnimation(hitName, true, false);
|
||||
return null;
|
||||
|
||||
if (hitSprite != null)
|
||||
{
|
||||
var strongHitSprite = this.GetAnimation($"{hitName}k", true, false);
|
||||
case TaikoSkinComponents.HitTarget:
|
||||
if (GetTexture("taikobigcircle") != null)
|
||||
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:
|
||||
// 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;
|
||||
|
||||
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:
|
||||
if (GetTexture("taiko-slider") != null)
|
||||
return new LegacyTaikoScroller();
|
||||
return null;
|
||||
|
||||
return null;
|
||||
case TaikoSkinComponents.BarLine:
|
||||
if (GetTexture("taiko-barline") != null)
|
||||
return new LegacyBarLine();
|
||||
|
||||
case TaikoSkinComponents.Mascot:
|
||||
return new DrawableTaikoMascot();
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.KiaiGlow:
|
||||
if (GetTexture("taiko-glow") != null)
|
||||
return new LegacyKiaiGlow();
|
||||
case TaikoSkinComponents.TaikoExplosionMiss:
|
||||
var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
|
||||
if (missSprite != null)
|
||||
return new LegacyHitExplosion(missSprite);
|
||||
|
||||
return null;
|
||||
return null;
|
||||
|
||||
default:
|
||||
throw new UnsupportedSkinComponentException(lookup);
|
||||
case TaikoSkinComponents.TaikoExplosionOk:
|
||||
case TaikoSkinComponents.TaikoExplosionGreat:
|
||||
string hitName = getHitName(taikoComponent.Component);
|
||||
var hitSprite = this.GetAnimation(hitName, true, false);
|
||||
|
||||
if (hitSprite != null)
|
||||
{
|
||||
var strongHitSprite = this.GetAnimation($"{hitName}k", true, false);
|
||||
|
||||
return new LegacyHitExplosion(hitSprite, strongHitSprite);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.TaikoExplosionKiai:
|
||||
// suppress the default kiai explosion if the skin brings its own sprites.
|
||||
// the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield.
|
||||
if (hasExplosion.Value)
|
||||
return Drawable.Empty().With(d => d.Expire());
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.Scroller:
|
||||
if (GetTexture("taiko-slider") != null)
|
||||
return new LegacyTaikoScroller();
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.Mascot:
|
||||
return new DrawableTaikoMascot();
|
||||
|
||||
case TaikoSkinComponents.KiaiGlow:
|
||||
if (GetTexture("taiko-glow") != null)
|
||||
return new LegacyKiaiGlow();
|
||||
|
||||
return null;
|
||||
|
||||
default:
|
||||
throw new UnsupportedSkinComponentException(lookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
using osu.Game.Rulesets.Taiko.Configuration;
|
||||
using osu.Game.Rulesets.Taiko.Edit.Setup;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko
|
||||
@@ -57,6 +58,9 @@ namespace osu.Game.Rulesets.Taiko
|
||||
case ArgonSkin:
|
||||
return new TaikoArgonSkinTransformer(skin);
|
||||
|
||||
case TrianglesSkin:
|
||||
return new TaikoTrianglesSkinTransformer(skin);
|
||||
|
||||
case LegacySkin:
|
||||
return new TaikoLegacySkinTransformer(skin);
|
||||
}
|
||||
@@ -130,6 +134,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
new TaikoModEasy(),
|
||||
new TaikoModNoFail(),
|
||||
new MultiMod(new TaikoModHalfTime(), new TaikoModDaycore()),
|
||||
new TaikoModSimplifiedRhythm(),
|
||||
};
|
||||
|
||||
case ModType.DifficultyIncrease:
|
||||
|
||||
BIN
osu.Game.Tests/Resources/Archives/modified-argon-20250424.osk
Normal file
BIN
osu.Game.Tests/Resources/Archives/modified-argon-20250424.osk
Normal file
Binary file not shown.
@@ -75,6 +75,8 @@ namespace osu.Game.Tests.Skins
|
||||
"Archives/modified-argon-20250116.osk",
|
||||
// Covers player team flag
|
||||
"Archives/modified-argon-20250214.osk",
|
||||
// Covers skinnable leaderboard
|
||||
"Archives/modified-argon-20250424.osk",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("toggle expanded", () =>
|
||||
{
|
||||
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);
|
||||
@@ -50,11 +50,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("add many scores in one go", () =>
|
||||
{
|
||||
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.
|
||||
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
|
||||
@@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
addLocalPlayer();
|
||||
|
||||
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]
|
||||
@@ -93,30 +93,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
createLeaderboard();
|
||||
addLocalPlayer();
|
||||
|
||||
AddStep("add peppy", () => createRandomScore(new APIUser { Username = "peppy", Id = 2 }));
|
||||
AddStep("add smoogipoo", () => createRandomScore(new APIUser { Username = "smoogipoo", Id = 1040328 }));
|
||||
AddStep("add flyte", () => createRandomScore(new APIUser { Username = "flyte", Id = 3103765 }));
|
||||
AddStep("add frenzibyte", () => 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);
|
||||
AddStep("add peppy", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = "peppy", Id = 2 }));
|
||||
AddStep("add smoogipoo", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = "smoogipoo", Id = 1040328 }));
|
||||
AddStep("add flyte", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = "flyte", Id = 3103765 }));
|
||||
AddStep("add frenzibyte", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = "frenzibyte", Id = 14210502 }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -143,12 +123,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
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",
|
||||
() => leaderboard.ChildrenOfType<Box>().Select(b => ((Colour4)b.Colour).ToHex()),
|
||||
() => 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",
|
||||
() => leaderboard.GetAllScoresForUsername("my friend")
|
||||
.SelectMany(score => score.ChildrenOfType<Box>())
|
||||
@@ -161,7 +141,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("add local player", () =>
|
||||
{
|
||||
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
|
||||
{
|
||||
public float Spacing => Flow.Spacing.Y;
|
||||
@@ -195,10 +167,20 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
=> 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 GameplayLeaderboardScore CreateRandomScore(APIUser user) => CreateLeaderboardScore(new BindableLong(RNG.Next(0, 5_000_000)), user);
|
||||
|
||||
public GameplayLeaderboardScore CreateLeaderboardScore(BindableLong totalScore, APIUser user, bool isTracked = false)
|
||||
{
|
||||
var score = new GameplayLeaderboardScore(user, isTracked, totalScore);
|
||||
Scores.Add(score);
|
||||
return score;
|
||||
}
|
||||
|
||||
IBindableList<GameplayLeaderboardScore> IGameplayLeaderboardProvider.Scores => Scores;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
@@ -41,6 +43,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager configManager { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@@ -49,6 +54,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
skins["legacy"] = new DefaultLegacySkin(this);
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddToggleStep("toggle leaderboard", b => configManager.SetValue(OsuSetting.GameplayLeaderboard, b));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLayout(
|
||||
[Values("argon", "triangles", "legacy")]
|
||||
|
||||
@@ -204,12 +204,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
Origin = Anchor.Centre,
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
new OsuSpriteText { Text = $@"Great: {hitWindows?.WindowFor(HitResult.Great)}" },
|
||||
new OsuSpriteText { Text = $@"Good: {hitWindows?.WindowFor(HitResult.Ok)}" },
|
||||
new OsuSpriteText { Text = $@"Meh: {hitWindows?.WindowFor(HitResult.Meh)}" },
|
||||
}
|
||||
ChildrenEnumerable = hitWindows?.GetAllAvailableWindows().Select(w => new OsuSpriteText { Text = $@"{w.result}: {w.length}" }) ?? []
|
||||
});
|
||||
|
||||
Add(new BarHitErrorMeter
|
||||
|
||||
@@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
public void TestScoreUpdates()
|
||||
{
|
||||
AddRepeatStep("update state", UpdateUserStatesRandomly, 100);
|
||||
AddToggleStep("switch compact mode", expanded => Leaderboard!.Expanded.Value = expanded);
|
||||
AddToggleStep("switch compact mode", expanded => Leaderboard!.ForceExpand.Value = expanded);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Expanded = { Value = true }
|
||||
ForceExpand = { Value = true }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -560,7 +560,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
|
||||
|
||||
private 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();
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Origin = Anchor.BottomCentre,
|
||||
Team1Score = { BindTarget = LeaderboardProvider.TeamScores[0] },
|
||||
Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] },
|
||||
Expanded = { BindTarget = Leaderboard!.Expanded },
|
||||
Expanded = { BindTarget = Leaderboard!.ForceExpand },
|
||||
}, Add);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
using osu.Game.Tests.Visual.Gameplay;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneMultiplayerPositionDisplay : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
private GameplayLeaderboardScore score = null!;
|
||||
|
||||
private readonly Bindable<int?> position = new Bindable<int?>(8);
|
||||
|
||||
private TestSceneGameplayLeaderboard.TestGameplayLeaderboardProvider leaderboardProvider = null!;
|
||||
private MultiplayerPositionDisplay display = null!;
|
||||
private GameplayState gameplayState = null!;
|
||||
|
||||
private const int player_count = 32;
|
||||
|
||||
[Test]
|
||||
public void TestAppearance()
|
||||
{
|
||||
AddStep("create content", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies =
|
||||
[
|
||||
(typeof(IGameplayLeaderboardProvider), leaderboardProvider = new TestSceneGameplayLeaderboard.TestGameplayLeaderboardProvider()),
|
||||
(typeof(GameplayState), gameplayState = TestGameplayState.Create(new OsuRuleset()))
|
||||
],
|
||||
Child = display = new MultiplayerPositionDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
score = leaderboardProvider.CreateLeaderboardScore(new BindableLong(), API.LocalUser.Value, true);
|
||||
score.Position.BindTo(position);
|
||||
|
||||
for (int i = 0; i < player_count - 1; i++)
|
||||
{
|
||||
var r = leaderboardProvider.CreateRandomScore(new APIUser());
|
||||
r.Position.Value = i;
|
||||
}
|
||||
});
|
||||
|
||||
AddSliderStep("set score position", 1, player_count, position.Value!.Value, r => position.Value = r);
|
||||
AddStep("unset position", () => position.Value = null);
|
||||
|
||||
AddStep("toggle leaderboardProvider on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true));
|
||||
AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1));
|
||||
|
||||
AddStep("toggle leaderboardProvider off", () => config.SetValue(OsuSetting.GameplayLeaderboard, false));
|
||||
AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0));
|
||||
|
||||
AddStep("enter break", () => ((Bindable<LocalUserPlayingState>)gameplayState.PlayingState).Value = LocalUserPlayingState.Break);
|
||||
AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1));
|
||||
|
||||
AddStep("exit break", () => ((Bindable<LocalUserPlayingState>)gameplayState.PlayingState).Value = LocalUserPlayingState.Playing);
|
||||
AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0));
|
||||
|
||||
AddStep("toggle leaderboardProvider on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true));
|
||||
AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1));
|
||||
|
||||
AddStep("change local user", () => ((DummyAPIAccess)API).LocalUser.Value = new GuestUser());
|
||||
AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Footer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
public partial class TestSceneScreenFooterNavigation : OsuGameTestScene
|
||||
{
|
||||
private ScreenFooter screenFooter => this.ChildrenOfType<ScreenFooter>().Single();
|
||||
|
||||
[Test]
|
||||
public void TestFooterHidesOldBackButton()
|
||||
{
|
||||
PushAndConfirm(() => new TestScreen(false));
|
||||
AddAssert("footer hidden", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
AddAssert("old back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
|
||||
PushAndConfirm(() => new TestScreen(true));
|
||||
AddAssert("footer shown", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
AddAssert("old back button hidden", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
|
||||
PushAndConfirm(() => new TestScreen(false));
|
||||
AddAssert("footer hidden", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
AddAssert("back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
|
||||
AddStep("exit screen", () => Game.ScreenStack.Exit());
|
||||
AddAssert("footer shown", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
AddAssert("old back button hidden", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
|
||||
AddStep("exit screen", () => Game.ScreenStack.Exit());
|
||||
AddAssert("footer hidden", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
AddAssert("old back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
}
|
||||
|
||||
private partial class TestScreen : OsuScreen
|
||||
{
|
||||
public override bool ShowFooter { get; }
|
||||
|
||||
public TestScreen(bool footer)
|
||||
{
|
||||
ShowFooter = footer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -611,7 +611,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
AddAssert("ensure score is databased", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == false));
|
||||
|
||||
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
|
||||
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action!.Invoke());
|
||||
|
||||
AddStep("show local scores",
|
||||
() => 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));
|
||||
|
||||
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
|
||||
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action!.Invoke());
|
||||
|
||||
AddStep("show local scores",
|
||||
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
|
||||
@@ -734,7 +734,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
public void TestPushSongSelectAndPressBackButtonImmediately()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -914,7 +914,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
|
||||
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);
|
||||
|
||||
AddStep("exit lounge", () => Game.ScreenStack.Exit());
|
||||
|
||||
@@ -75,9 +75,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[TestCase("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", LinkAction.External)]
|
||||
[TestCase("(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("Join my multiplayer game osump://12346.", LinkAction.JoinMultiplayerMatch)]
|
||||
[TestCase("Join my multiplayer gameosump://12346.", LinkAction.JoinMultiplayerMatch)]
|
||||
[TestCase("Join my [multiplayer game](osump://12346).", LinkAction.JoinMultiplayerMatch)]
|
||||
[TestCase("Join my multiplayer game osu://room/12346.", LinkAction.JoinRoom)]
|
||||
[TestCase("Join my multiplayer gameosu://room/12346.", LinkAction.JoinRoom)]
|
||||
[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 {OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)]
|
||||
[TestCase($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)]
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Game.Overlays.Comments;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
[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
|
||||
{
|
||||
[Test]
|
||||
|
||||
@@ -167,6 +167,57 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
AddAssert("local score is #16", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16));
|
||||
}
|
||||
|
||||
[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]
|
||||
public void TestOnlineLeaderboardWithLessThan50Scores_UserIsLast()
|
||||
{
|
||||
@@ -207,7 +258,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineLeaderboardWithMoreThan50Scores_UserOutsideOfTop50()
|
||||
public void TestOnlineLeaderboardWithMoreThan50Scores_UserOutsideOfTop50_DidNotBeatOwnBest()
|
||||
{
|
||||
ScoreInfo localScore = null!;
|
||||
|
||||
@@ -227,15 +278,69 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||
}
|
||||
|
||||
var userBest = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
var userBest = SoloScoreInfo.ForSubmission(TestResources.CreateTestScoreInfo(importedBeatmap));
|
||||
userBest.TotalScore = 50_000;
|
||||
userBest.ID = 123456;
|
||||
|
||||
getScoresRequest.TriggerSuccess(new APIScoresCollection
|
||||
{
|
||||
Scores = scores,
|
||||
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,
|
||||
}
|
||||
});
|
||||
@@ -254,7 +359,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
});
|
||||
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("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]
|
||||
@@ -278,15 +383,17 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
scores.Add(SoloScoreInfo.ForSubmission(score));
|
||||
}
|
||||
|
||||
var userBest = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
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 = SoloScoreInfo.ForSubmission(userBest),
|
||||
Score = userBest,
|
||||
Position = 133_337,
|
||||
}
|
||||
});
|
||||
@@ -305,7 +412,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
});
|
||||
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("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]
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class BeatmapCarouselFilterSortingTest
|
||||
{
|
||||
[Test]
|
||||
public async Task TestSorting()
|
||||
{
|
||||
List<BeatmapSetInfo> beatmapSets = new List<BeatmapSetInfo>();
|
||||
|
||||
const string zzz_lowercase = "zzzzz";
|
||||
const string zzz_uppercase = "ZZZZZ";
|
||||
const int diff_count = 5;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||
|
||||
if (i == 4)
|
||||
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_uppercase);
|
||||
|
||||
if (i == 8)
|
||||
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_lowercase);
|
||||
|
||||
if (i == 12)
|
||||
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_uppercase);
|
||||
|
||||
if (i == 16)
|
||||
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_lowercase);
|
||||
|
||||
beatmapSets.Add(set);
|
||||
}
|
||||
|
||||
var results = await runSorting(SortMode.Author, beatmapSets);
|
||||
|
||||
Assert.That(results.Last().Metadata.Author.Username, Is.EqualTo(zzz_uppercase));
|
||||
Assert.That(results.SkipLast(diff_count).Last().Metadata.Author.Username, Is.EqualTo(zzz_lowercase));
|
||||
|
||||
results = await runSorting(SortMode.Artist, beatmapSets);
|
||||
|
||||
Assert.That(results.Last().Metadata.Artist, Is.EqualTo(zzz_uppercase));
|
||||
Assert.That(results.SkipLast(diff_count).Last().Metadata.Artist, Is.EqualTo(zzz_lowercase));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSortingDateSubmitted()
|
||||
{
|
||||
List<BeatmapSetInfo> beatmapSets = new List<BeatmapSetInfo>();
|
||||
|
||||
const string zzz_string = "zzzzz";
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var set = TestResources.CreateTestBeatmapSetInfo(5);
|
||||
|
||||
// A total of 6 sets have date submitted (4 don't)
|
||||
// A total of 5 sets have artist string (3 of which also have date submitted)
|
||||
|
||||
if (i >= 2 && i < 8) // i = 2, 3, 4, 5, 6, 7 have submitted date
|
||||
set.DateSubmitted = DateTimeOffset.Now.AddMinutes(i);
|
||||
if (i < 5) // i = 0, 1, 2, 3, 4 have matching string
|
||||
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string);
|
||||
|
||||
set.Beatmaps.ForEach(b => b.Metadata.Title = $"submitted: {set.DateSubmitted}");
|
||||
|
||||
beatmapSets.Add(set);
|
||||
}
|
||||
|
||||
var results = await runSorting(SortMode.DateSubmitted, beatmapSets);
|
||||
|
||||
Assert.That(results.Count(), Is.EqualTo(50));
|
||||
|
||||
Assert.That(results.Reverse().TakeWhile(b => b.BeatmapSet!.DateSubmitted == null).Count(), Is.EqualTo(20), () => "missing dates should be at the end");
|
||||
Assert.That(results.TakeWhile(b => b.BeatmapSet!.DateSubmitted != null).Count(), Is.EqualTo(30), () => "non-missing dates should be at the start");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSortByArtistUsesTitleAsTiebreaker()
|
||||
{
|
||||
List<BeatmapSetInfo> beatmapSets = new List<BeatmapSetInfo>();
|
||||
|
||||
const int diff_count = 5;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||
|
||||
if (i == 4)
|
||||
{
|
||||
set.Beatmaps.ForEach(b =>
|
||||
{
|
||||
b.Metadata.Artist = "ZZZ";
|
||||
b.Metadata.Title = "AAA";
|
||||
});
|
||||
}
|
||||
|
||||
if (i == 8)
|
||||
{
|
||||
set.Beatmaps.ForEach(b =>
|
||||
{
|
||||
b.Metadata.Artist = "ZZZ";
|
||||
b.Metadata.Title = "ZZZ";
|
||||
});
|
||||
}
|
||||
|
||||
beatmapSets.Add(set);
|
||||
}
|
||||
|
||||
var results = await runSorting(SortMode.Artist, beatmapSets);
|
||||
|
||||
Assert.That(() =>
|
||||
{
|
||||
var lastItem = results.Last();
|
||||
return lastItem.Metadata.Artist == "ZZZ" && lastItem.Metadata.Title == "ZZZ";
|
||||
});
|
||||
|
||||
Assert.That(() =>
|
||||
{
|
||||
var secondLastItem = results.SkipLast(diff_count).Last();
|
||||
return secondLastItem.Metadata.Artist == "ZZZ" && secondLastItem.Metadata.Title == "AAA";
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures stability is maintained on different sort modes for items with equal properties.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestSortingStabilityDateAdded()
|
||||
{
|
||||
List<BeatmapSetInfo> beatmapSets = new List<BeatmapSetInfo>();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var set = TestResources.CreateTestBeatmapSetInfo();
|
||||
|
||||
set.DateAdded = DateTimeOffset.FromUnixTimeSeconds(i);
|
||||
|
||||
// only need to set the first as they are a shared reference.
|
||||
var beatmap = set.Beatmaps.First();
|
||||
|
||||
beatmap.Metadata.Artist = "a";
|
||||
beatmap.Metadata.Title = "b";
|
||||
|
||||
beatmapSets.Add(set);
|
||||
}
|
||||
|
||||
var results = await runSorting(SortMode.Title, beatmapSets);
|
||||
|
||||
Assert.That(results.Select(b => b.BeatmapSet!.DateAdded), Is.Ordered.Descending);
|
||||
|
||||
results = await runSorting(SortMode.Artist, beatmapSets);
|
||||
|
||||
Assert.That(results.Select(b => b.BeatmapSet!.DateAdded), Is.Ordered.Descending);
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<BeatmapInfo>> runSorting(SortMode sort, List<BeatmapSetInfo> beatmapSets)
|
||||
{
|
||||
var sorter = new BeatmapCarouselFilterSorting(() => new FilterCriteria { Sort = sort });
|
||||
var carouselItems = await sorter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))), CancellationToken.None);
|
||||
return carouselItems.Select(ci => ci.Model).OfType<BeatmapInfo>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -20,6 +22,7 @@ using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
@@ -34,7 +37,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
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();
|
||||
|
||||
@@ -48,6 +51,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
private int beatmapCount;
|
||||
|
||||
protected int NewItemsPresentedInvocationCount;
|
||||
|
||||
protected BeatmapCarouselTestScene()
|
||||
{
|
||||
store = new TestBeatmapStore
|
||||
@@ -64,6 +69,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
AddStep("create components", () =>
|
||||
{
|
||||
NewItemsPresentedInvocationCount = 0;
|
||||
|
||||
Box topBox;
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@@ -95,8 +102,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
Carousel = new BeatmapCarousel
|
||||
Carousel = new TestBeatmapCarousel
|
||||
{
|
||||
NewItemsPresented = () => NewItemsPresentedInvocationCount++,
|
||||
BleedTop = 50,
|
||||
BleedBottom = 50,
|
||||
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 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 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 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? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
|
||||
|
||||
@@ -296,5 +353,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public partial class TestBeatmapCarousel : BeatmapCarousel
|
||||
{
|
||||
public IEnumerable<BeatmapInfo> PostFilterBeatmaps = null!;
|
||||
|
||||
protected override Task<IEnumerable<CarouselItem>> FilterAsync()
|
||||
{
|
||||
var filterAsync = base.FilterAsync();
|
||||
filterAsync.ContinueWith(result =>
|
||||
{
|
||||
if (result.IsCompletedSuccessfully)
|
||||
PostFilterBeatmaps = result.GetResultSafely().Select(i => i.Model).OfType<BeatmapInfo>();
|
||||
});
|
||||
return filterAsync;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
204
osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs
Normal file
204
osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Toolbar;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public abstract partial class SongSelectTestScene : ScreenTestScene
|
||||
{
|
||||
protected BeatmapManager Beatmaps { get; private set; } = null!;
|
||||
protected RealmRulesetStore Rulesets { get; private set; } = null!;
|
||||
protected OsuConfigManager Config { get; private set; } = null!;
|
||||
protected ScoreManager ScoreManager { get; private set; } = null!;
|
||||
|
||||
private RealmDetachedBeatmapStore beatmapStore = null!;
|
||||
|
||||
protected Screens.SelectV2.SongSelect SongSelect { get; private set; } = null!;
|
||||
protected BeatmapCarousel Carousel => SongSelect.ChildrenOfType<BeatmapCarousel>().Single();
|
||||
|
||||
[Cached]
|
||||
protected readonly ScreenFooter Footer;
|
||||
|
||||
[Cached]
|
||||
private readonly OsuLogo logo;
|
||||
|
||||
[Cached]
|
||||
private readonly VolumeOverlay volume;
|
||||
|
||||
[Cached(typeof(INotificationOverlay))]
|
||||
private readonly INotificationOverlay notificationOverlay = new NotificationOverlay();
|
||||
|
||||
[Cached]
|
||||
protected readonly LeaderboardManager LeaderboardManager = new LeaderboardManager();
|
||||
|
||||
protected SongSelectTestScene()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
LeaderboardManager,
|
||||
new Toolbar
|
||||
{
|
||||
State = { Value = Visibility.Visible },
|
||||
},
|
||||
Footer = new ScreenFooter
|
||||
{
|
||||
BackButtonPressed = () => Stack.CurrentScreen.Exit(),
|
||||
},
|
||||
logo = new OsuLogo
|
||||
{
|
||||
Alpha = 0f,
|
||||
},
|
||||
volume = new VolumeOverlay(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Stack.Padding = new MarginPadding { Top = Toolbar.HEIGHT };
|
||||
}
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
// These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install.
|
||||
// At a point we have isolated interactive test runs enough, this can likely be removed.
|
||||
dependencies.Cache(Rulesets = new RealmRulesetStore(Realm));
|
||||
dependencies.Cache(Realm);
|
||||
dependencies.Cache(Beatmaps = new BeatmapManager(LocalStorage, Realm, null, Dependencies.Get<AudioManager>(), Resources, Dependencies.Get<GameHost>(), Beatmap.Default));
|
||||
dependencies.Cache(Config = new OsuConfigManager(LocalStorage));
|
||||
dependencies.Cache(ScoreManager = new ScoreManager(Rulesets, () => Beatmaps, LocalStorage, Realm, API, Config));
|
||||
|
||||
dependencies.CacheAs<BeatmapStore>(beatmapStore = new RealmDetachedBeatmapStore());
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Add(beatmapStore);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Stack.ScreenPushed += updateFooter;
|
||||
Stack.ScreenExited += updateFooter;
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("reset defaults", () =>
|
||||
{
|
||||
Ruleset.Value = Rulesets.AvailableRulesets.First();
|
||||
|
||||
Beatmap.SetDefault();
|
||||
SelectedMods.SetDefault();
|
||||
|
||||
Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title);
|
||||
Config.SetValue(OsuSetting.SongSelectGroupingMode, GroupMode.All);
|
||||
|
||||
SongSelect = null!;
|
||||
});
|
||||
|
||||
AddStep("delete all beatmaps", () => Beatmaps.Delete());
|
||||
}
|
||||
|
||||
protected virtual void LoadSongSelect()
|
||||
{
|
||||
AddStep("load screen", () => Stack.Push(SongSelect = new SoloSongSelect()));
|
||||
AddUntilStep("wait for load", () => Stack.CurrentScreen == SongSelect && SongSelect.IsLoaded);
|
||||
AddUntilStep("wait for filtering", () => !Carousel.IsFiltering);
|
||||
}
|
||||
|
||||
protected void ImportBeatmapForRuleset(int rulesetId)
|
||||
{
|
||||
int beatmapsCount = 0;
|
||||
|
||||
AddStep($"import test map for ruleset {rulesetId}", () =>
|
||||
{
|
||||
beatmapsCount = SongSelect.IsNull() ? 0 : Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single().SetItems.Count;
|
||||
Beatmaps.Import(TestResources.CreateTestBeatmapSetInfo(3, Rulesets.AvailableRulesets.Where(r => r.OnlineID == rulesetId).ToArray()));
|
||||
});
|
||||
|
||||
// This is specifically for cases where the add is happening post song select load.
|
||||
// For cases where song select is null, the assertions are provided by the load checks.
|
||||
AddUntilStep("wait for imported to arrive in carousel", () => SongSelect.IsNull() || Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single().SetItems.Count > beatmapsCount);
|
||||
}
|
||||
|
||||
protected void ChangeMods(params Mod[] mods) => AddStep($"change mods to {string.Join(", ", mods.Select(m => m.Acronym))}", () => SelectedMods.Value = mods);
|
||||
|
||||
protected void ChangeRuleset(int rulesetId)
|
||||
{
|
||||
AddStep($"change ruleset to {rulesetId}", () => Ruleset.Value = Rulesets.AvailableRulesets.First(r => r.OnlineID == rulesetId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports test beatmap sets to show in the carousel.
|
||||
/// </summary>
|
||||
/// <param name="difficultyCountPerSet">
|
||||
/// The exact count of difficulties to create for each beatmap set.
|
||||
/// A <see langword="null"/> value causes the count of difficulties to be selected randomly.
|
||||
/// </param>
|
||||
protected void AddManyTestMaps(int? difficultyCountPerSet = null)
|
||||
{
|
||||
AddStep("import test maps", () =>
|
||||
{
|
||||
var usableRulesets = Rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
Beatmaps.Import(TestResources.CreateTestBeatmapSetInfo(difficultyCountPerSet, usableRulesets));
|
||||
});
|
||||
}
|
||||
|
||||
protected void WaitForSuspension() => AddUntilStep("wait for not current", () => !SongSelect.AsNonNull().IsCurrentScreen());
|
||||
|
||||
private void updateFooter(IScreen? _, IScreen? newScreen)
|
||||
{
|
||||
if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter)
|
||||
{
|
||||
Footer.Show();
|
||||
Footer.SetButtons(osuScreen.CreateFooterButtons());
|
||||
}
|
||||
else
|
||||
{
|
||||
Footer.Hide();
|
||||
Footer.SetButtons(Array.Empty<ScreenFooterButton>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
@@ -34,9 +33,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
[Explicit]
|
||||
public void TestSorting()
|
||||
{
|
||||
SortBy(new FilterCriteria { Sort = SortMode.Artist });
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist });
|
||||
SortAndGroupBy(SortMode.Artist, GroupMode.All);
|
||||
SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty);
|
||||
SortAndGroupBy(SortMode.Artist, GroupMode.Artist);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -5,9 +5,10 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
@@ -19,7 +20,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
CreateCarousel();
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist });
|
||||
|
||||
SortAndGroupBy(SortMode.Artist, GroupMode.Artist);
|
||||
|
||||
AddBeatmaps(10, 3, true);
|
||||
WaitForDrawablePanels();
|
||||
@@ -173,5 +175,71 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(1, 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInputHandlingWithinGaps()
|
||||
{
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
||||
|
||||
// Clicks just above the first group panel should not actuate any action.
|
||||
ClickVisiblePanelWithOffset<PanelGroup>(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 + 1)));
|
||||
|
||||
AddAssert("no sets visible", () => !GetVisiblePanels<PanelBeatmapSet>().Any());
|
||||
|
||||
// add lenience to avoid floating-point inaccuracies at edge.
|
||||
ClickVisiblePanelWithOffset<PanelGroup>(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 - 1)));
|
||||
|
||||
AddUntilStep("wait for sets visible", () => GetVisiblePanels<PanelBeatmapSet>().Any());
|
||||
CheckNoSelection();
|
||||
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
||||
|
||||
ClickVisiblePanelWithOffset<PanelBeatmapSet>(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<PanelBeatmap>().Any());
|
||||
|
||||
// Beatmap panels expand their selection area to cover holes from spacing.
|
||||
ClickVisiblePanelWithOffset<PanelBeatmap>(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
ClickVisiblePanelWithOffset<PanelBeatmap>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForGroupSelection(0, 2);
|
||||
|
||||
ClickVisiblePanelWithOffset<PanelBeatmapSet>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForGroupSelection(0, 5);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicFiltering()
|
||||
{
|
||||
ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
|
||||
WaitForFiltering();
|
||||
|
||||
CheckDisplayedGroupsCount(1);
|
||||
CheckDisplayedBeatmapSetsCount(1);
|
||||
CheckDisplayedBeatmapsCount(3);
|
||||
|
||||
CheckNoSelection();
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
SelectNextPanel();
|
||||
|
||||
Select();
|
||||
|
||||
WaitForGroupSelection(0, 2);
|
||||
|
||||
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||
WaitForFiltering();
|
||||
|
||||
CheckDisplayedGroupsCount(5);
|
||||
CheckDisplayedBeatmapSetsCount(10);
|
||||
CheckDisplayedBeatmapsCount(30);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osuTK;
|
||||
@@ -21,7 +20,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
CreateCarousel();
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
|
||||
|
||||
SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty);
|
||||
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
@@ -30,32 +30,32 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
[Test]
|
||||
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();
|
||||
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
ClickVisiblePanel<PanelGroupStarDifficulty>(0);
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmapStandalone>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
CheckNoSelection();
|
||||
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
ClickVisiblePanel<PanelGroupStarDifficulty>(0);
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmapStandalone>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
}
|
||||
|
||||
[Test]
|
||||
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();
|
||||
|
||||
SelectNextPanel();
|
||||
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);
|
||||
CheckNoSelection();
|
||||
|
||||
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);
|
||||
CheckNoSelection();
|
||||
}
|
||||
@@ -88,10 +88,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
ClickVisiblePanel<PanelGroupStarDifficulty>(0);
|
||||
AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null);
|
||||
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
ClickVisiblePanel<PanelGroupStarDifficulty>(0);
|
||||
AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
}
|
||||
|
||||
@@ -121,18 +121,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf<PanelBeatmap>);
|
||||
AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf<PanelBeatmap>);
|
||||
AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf<PanelBeatmapStandalone>);
|
||||
AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf<PanelBeatmapStandalone>);
|
||||
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<PanelGroup>);
|
||||
ClickVisiblePanel<PanelGroupStarDifficulty>(0);
|
||||
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<PanelGroupStarDifficulty>);
|
||||
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<PanelGroup>);
|
||||
ClickVisiblePanel<PanelGroupStarDifficulty>(0);
|
||||
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<PanelGroupStarDifficulty>);
|
||||
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]
|
||||
@@ -147,7 +147,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
// open first group
|
||||
Select();
|
||||
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();
|
||||
Select();
|
||||
@@ -172,24 +172,57 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
[Test]
|
||||
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.
|
||||
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();
|
||||
|
||||
// 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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicFiltering()
|
||||
{
|
||||
ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
|
||||
WaitForFiltering();
|
||||
|
||||
CheckDisplayedGroupsCount(3);
|
||||
CheckDisplayedBeatmapsCount(3);
|
||||
|
||||
CheckNoSelection();
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
SelectNextPanel();
|
||||
|
||||
Select();
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
|
||||
WaitForGroupSelection(1, 0);
|
||||
|
||||
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||
WaitForFiltering();
|
||||
|
||||
CheckDisplayedGroupsCount(3);
|
||||
CheckDisplayedBeatmapsCount(30);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapCarouselFiltering : BeatmapCarouselTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
CreateCarousel();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicFiltering()
|
||||
{
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(1));
|
||||
|
||||
ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(2));
|
||||
|
||||
CheckDisplayedBeatmapSetsCount(1);
|
||||
CheckDisplayedBeatmapsCount(3);
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
|
||||
WaitForSelection(2, 0);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
SelectNextPanel();
|
||||
|
||||
Select();
|
||||
WaitForSelection(2, 1);
|
||||
|
||||
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(3));
|
||||
|
||||
CheckDisplayedBeatmapSetsCount(10);
|
||||
CheckDisplayedBeatmapsCount(30);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFilteringByUserStarDifficulty()
|
||||
{
|
||||
AddStep("add mixed difficulty set", () =>
|
||||
{
|
||||
var set = TestResources.CreateTestBeatmapSetInfo(1);
|
||||
set.Beatmaps.Clear();
|
||||
|
||||
for (int i = 1; i <= 15; i++)
|
||||
{
|
||||
set.Beatmaps.Add(new BeatmapInfo(new OsuRuleset().RulesetInfo, new BeatmapDifficulty(), new BeatmapMetadata())
|
||||
{
|
||||
BeatmapSet = set,
|
||||
DifficultyName = $"Stars: {i}",
|
||||
StarRating = i,
|
||||
});
|
||||
}
|
||||
|
||||
BeatmapSets.Add(set);
|
||||
});
|
||||
|
||||
WaitForDrawablePanels();
|
||||
|
||||
ApplyToFilter("filter [5..]", c =>
|
||||
{
|
||||
c.UserStarDifficulty.Min = 5;
|
||||
c.UserStarDifficulty.Max = null;
|
||||
});
|
||||
WaitForFiltering();
|
||||
CheckDisplayedBeatmapsCount(11);
|
||||
|
||||
ApplyToFilter("filter to [0..7]", c =>
|
||||
{
|
||||
c.UserStarDifficulty.Min = null;
|
||||
c.UserStarDifficulty.Max = 7;
|
||||
});
|
||||
WaitForFiltering();
|
||||
CheckDisplayedBeatmapsCount(7);
|
||||
|
||||
ApplyToFilter("filter to [5..7]", c =>
|
||||
{
|
||||
c.UserStarDifficulty.Min = 5;
|
||||
c.UserStarDifficulty.Max = 7;
|
||||
});
|
||||
|
||||
WaitForFiltering();
|
||||
CheckDisplayedBeatmapsCount(3);
|
||||
|
||||
ApplyToFilter("filter to [2..2]", c =>
|
||||
{
|
||||
c.UserStarDifficulty.Min = 2;
|
||||
c.UserStarDifficulty.Max = 2;
|
||||
});
|
||||
|
||||
WaitForFiltering();
|
||||
CheckDisplayedBeatmapsCount(1);
|
||||
|
||||
ApplyToFilter("filter to [0..]", c =>
|
||||
{
|
||||
c.UserStarDifficulty.Min = 0;
|
||||
c.UserStarDifficulty.Max = null;
|
||||
});
|
||||
WaitForFiltering();
|
||||
CheckDisplayedBeatmapsCount(15);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCarouselRemembersSelection()
|
||||
{
|
||||
Guid selectedID = Guid.Empty;
|
||||
|
||||
AddBeatmaps(50, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextGroup();
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
|
||||
AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
ApplyToFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString());
|
||||
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCarouselRemembersSelectionDifficultySort()
|
||||
{
|
||||
Guid selectedID = Guid.Empty;
|
||||
|
||||
AddBeatmaps(50, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SortBy(SortMode.Difficulty);
|
||||
|
||||
SelectNextGroup();
|
||||
|
||||
AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
ApplyToFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString());
|
||||
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
|
||||
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCarouselRetainsSelectionFromDifficultySort()
|
||||
{
|
||||
AddBeatmaps(50, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
BeatmapInfo chosenBeatmap = null!;
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
int diff = i;
|
||||
|
||||
AddStep($"select diff {diff}", () => Carousel.CurrentSelection = chosenBeatmap = BeatmapSets[20].Beatmaps[diff]);
|
||||
AddUntilStep("selection changed", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
|
||||
|
||||
SortBy(SortMode.Difficulty);
|
||||
AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
|
||||
|
||||
SortBy(SortMode.Title);
|
||||
AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExternalRulesetChange()
|
||||
{
|
||||
ApplyToFilter("allow converted beatmaps", c => c.AllowConvertedBeatmaps = true);
|
||||
ApplyToFilter("filter to osu", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(0));
|
||||
|
||||
WaitForFiltering();
|
||||
|
||||
AddStep("add mixed ruleset beatmapset", () =>
|
||||
{
|
||||
var testMixed = TestResources.CreateTestBeatmapSetInfo(3);
|
||||
|
||||
for (int i = 0; i <= 2; i++)
|
||||
testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i);
|
||||
|
||||
BeatmapSets.Add(testMixed);
|
||||
});
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
|
||||
AddUntilStep("wait for filtered difficulties", () =>
|
||||
{
|
||||
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
|
||||
|
||||
return visibleBeatmapPanels.Count() == 1
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1;
|
||||
});
|
||||
|
||||
ApplyToFilter("filter to taiko", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(1));
|
||||
|
||||
WaitForFiltering();
|
||||
|
||||
AddUntilStep("wait for filtered difficulties", () =>
|
||||
{
|
||||
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
|
||||
|
||||
return visibleBeatmapPanels.Count() == 2
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 1) == 1;
|
||||
});
|
||||
|
||||
ApplyToFilter("filter to catch", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(2));
|
||||
|
||||
WaitForFiltering();
|
||||
|
||||
AddUntilStep("wait for filtered difficulties", () =>
|
||||
{
|
||||
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
|
||||
|
||||
return visibleBeatmapPanels.Count() == 2
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 2) == 1;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSortingWithDifficultyFiltered()
|
||||
{
|
||||
const int diffs_per_set = 3;
|
||||
const int local_set_count = 2;
|
||||
|
||||
AddStep("populate beatmap sets", () =>
|
||||
{
|
||||
for (int i = 0; i < local_set_count; i++)
|
||||
{
|
||||
var set = TestResources.CreateTestBeatmapSetInfo(diffs_per_set);
|
||||
set.Beatmaps[0].StarRating = 3 - i;
|
||||
set.Beatmaps[0].DifficultyName += $" ({3 - i}*)";
|
||||
set.Beatmaps[1].StarRating = 6 + i;
|
||||
set.Beatmaps[1].DifficultyName += $" ({6 + i}*)";
|
||||
BeatmapSets.Add(set);
|
||||
}
|
||||
});
|
||||
|
||||
SortBy(SortMode.Difficulty);
|
||||
WaitForFiltering();
|
||||
|
||||
CheckDisplayedBeatmapsCount(local_set_count * diffs_per_set);
|
||||
|
||||
ApplyToFilter("filter to normal", c => c.SearchText = "Normal");
|
||||
WaitForFiltering();
|
||||
|
||||
CheckDisplayedBeatmapsCount(local_set_count);
|
||||
|
||||
ApplyToFilter("filter to insane", c => c.SearchText = "Insane");
|
||||
WaitForFiltering();
|
||||
|
||||
CheckDisplayedBeatmapsCount(local_set_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osuTK;
|
||||
@@ -22,7 +21,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
CreateCarousel();
|
||||
SortBy(new FilterCriteria { Sort = SortMode.Title });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -221,7 +219,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
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());
|
||||
WaitForSelection(0, 0);
|
||||
@@ -238,6 +237,30 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
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)
|
||||
{
|
||||
object? selection = null;
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
@@ -18,37 +17,59 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
CreateCarousel();
|
||||
SortBy(new FilterCriteria());
|
||||
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScrollPositionMaintainedOnAddSecondSelected()
|
||||
public void TestScrollPositionMaintainedOnRemove_SecondSelected()
|
||||
{
|
||||
Quad positionBefore = default;
|
||||
|
||||
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();
|
||||
|
||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||
|
||||
RemoveFirstBeatmap();
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
() => Is.EqualTo(positionBefore));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScrollPositionMaintainedOnAddLastSelected()
|
||||
public void TestScrollPositionMaintainedOnRemove_SecondSelected_WithUserScroll()
|
||||
{
|
||||
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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
RemoveFirstBeatmap();
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
() => Is.EqualTo(positionBefore));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScrollToSelectionAfterFilter()
|
||||
{
|
||||
Quad positionBefore = default;
|
||||
|
||||
AddStep("select first beatmap", () => Carousel.CurrentSelection = BeatmapSets.First().Beatmaps.First());
|
||||
|
||||
WaitForScrolling();
|
||||
|
||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||
|
||||
AddStep("scroll to end", () => Scroll.ScrollToEnd());
|
||||
WaitForScrolling();
|
||||
|
||||
ApplyToFilter("search", f => f.SearchText = "Some");
|
||||
WaitForFiltering();
|
||||
|
||||
AddUntilStep("select screen position returned to selection", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
() => Is.EqualTo(positionBefore));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScrollToSelectionAfterFilter_WithUserScroll()
|
||||
{
|
||||
Quad positionBefore = default;
|
||||
|
||||
AddStep("select first beatmap", () => Carousel.CurrentSelection = BeatmapSets.First().Beatmaps.First());
|
||||
WaitForScrolling();
|
||||
|
||||
AddStep("override scroll with user scroll", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Scroll.ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.ScrollVerticalBy(-1);
|
||||
});
|
||||
WaitForScrolling();
|
||||
|
||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||
|
||||
ApplyToFilter("search", f => f.SearchText = "Some");
|
||||
WaitForFiltering();
|
||||
|
||||
AddUntilStep("select screen position returned to selection", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
() => Is.EqualTo(positionBefore));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
@@ -39,7 +40,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
BeatmapSets.Add(baseTestBeatmap);
|
||||
});
|
||||
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -55,7 +56,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
AddStep("update beatmap with same reference", () => BeatmapSets.ReplaceRange(1, 1, [baseTestBeatmap]));
|
||||
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
AddAssert("drawables unchanged", () => Carousel.ChildrenOfType<Panel>(), () => Is.EqualTo(originalDrawables));
|
||||
}
|
||||
|
||||
@@ -78,21 +79,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
updateBeatmap(b => b.Metadata = metadata);
|
||||
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
AddAssert("drawables changed", () => Carousel.ChildrenOfType<Panel>(), () => Is.Not.EqualTo(originalDrawables));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectionHeld()
|
||||
{
|
||||
SelectPrevGroup();
|
||||
SelectNextGroup();
|
||||
|
||||
WaitForSelection(1, 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]));
|
||||
|
||||
updateBeatmap();
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
|
||||
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]));
|
||||
@@ -101,14 +102,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
[Test] // Checks that we keep selection based on online ID where possible.
|
||||
public void TestSelectionHeldDifficultyNameChanged()
|
||||
{
|
||||
SelectPrevGroup();
|
||||
SelectNextGroup();
|
||||
|
||||
WaitForSelection(1, 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]));
|
||||
|
||||
updateBeatmap(b => b.DifficultyName = "new name");
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
|
||||
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]));
|
||||
@@ -117,19 +118,137 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
[Test] // Checks that we fallback to keeping selection based on difficulty name.
|
||||
public void TestSelectionHeldDifficultyOnlineIDChanged()
|
||||
{
|
||||
SelectPrevGroup();
|
||||
SelectNextGroup();
|
||||
|
||||
WaitForSelection(1, 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]));
|
||||
|
||||
updateBeatmap(b => b.OnlineID = b.OnlineID + 1);
|
||||
WaitForSorting();
|
||||
WaitForFiltering();
|
||||
|
||||
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]));
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
AddStep("update beatmap with different reference", () =>
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneBeatmapFilterControl : SongSelectComponentsTestScene
|
||||
{
|
||||
private FilterControl filterControl = null!;
|
||||
|
||||
protected override Anchor ComponentAnchor => Anchor.TopRight;
|
||||
protected override float InitialRelativeWidth => 0.7f;
|
||||
|
||||
@@ -20,12 +22,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Child = new FilterControl
|
||||
Child = filterControl = new FilterControl
|
||||
{
|
||||
State = { Value = Visibility.Visible },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestSearch()
|
||||
{
|
||||
AddStep("search for text", () => filterControl.Search("test search"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneFooterButtonMods : OsuTestScene
|
||||
{
|
||||
private readonly TestScreenFooterButtonMods footerButtonMods;
|
||||
private readonly FooterButtonMods footerButtonMods;
|
||||
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
public TestSceneFooterButtonMods()
|
||||
{
|
||||
Add(footerButtonMods = new TestScreenFooterButtonMods(new TestModSelectOverlay())
|
||||
Add(footerButtonMods = new FooterButtonMods(new TestModSelectOverlay())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.CentreLeft,
|
||||
@@ -63,19 +63,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
var hiddenMod = new Mod[] { new OsuModHidden() };
|
||||
AddStep(@"Add Hidden", () => changeMods(hiddenMod));
|
||||
AddAssert(@"Check Hidden multiplier", () => assertModsMultiplier(hiddenMod));
|
||||
assertModsMultiplier(hiddenMod);
|
||||
|
||||
var hardRockMod = new Mod[] { new OsuModHardRock() };
|
||||
AddStep(@"Add HardRock", () => changeMods(hardRockMod));
|
||||
AddAssert(@"Check HardRock multiplier", () => assertModsMultiplier(hardRockMod));
|
||||
assertModsMultiplier(hardRockMod);
|
||||
|
||||
var doubleTimeMod = new Mod[] { new OsuModDoubleTime() };
|
||||
AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod));
|
||||
AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod));
|
||||
assertModsMultiplier(doubleTimeMod);
|
||||
|
||||
var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
|
||||
AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods));
|
||||
AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods));
|
||||
assertModsMultiplier(multipleIncrementMods);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -83,15 +83,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
var easyMod = new Mod[] { new OsuModEasy() };
|
||||
AddStep(@"Add Easy", () => changeMods(easyMod));
|
||||
AddAssert(@"Check Easy multiplier", () => assertModsMultiplier(easyMod));
|
||||
assertModsMultiplier(easyMod);
|
||||
|
||||
var noFailMod = new Mod[] { new OsuModNoFail() };
|
||||
AddStep(@"Add NoFail", () => changeMods(noFailMod));
|
||||
AddAssert(@"Check NoFail multiplier", () => assertModsMultiplier(noFailMod));
|
||||
assertModsMultiplier(noFailMod);
|
||||
|
||||
var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() };
|
||||
AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods));
|
||||
AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleDecrementMods));
|
||||
assertModsMultiplier(multipleDecrementMods);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -105,12 +105,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
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);
|
||||
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
|
||||
@@ -121,15 +121,5 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
ShowPresets = true;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class TestScreenFooterButtonMods : FooterButtonMods
|
||||
{
|
||||
public new OsuSpriteText MultiplierText => base.MultiplierText;
|
||||
|
||||
public TestScreenFooterButtonMods(ModSelectOverlay overlay)
|
||||
: base(overlay)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
@@ -55,21 +56,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
new PanelGroupStarDifficulty
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition(star, star.ToString()))
|
||||
Item = new CarouselItem(new GroupDefinition(new StarDifficulty(star, 0), $"{star} Star(s)"))
|
||||
},
|
||||
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 },
|
||||
},
|
||||
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 },
|
||||
},
|
||||
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 },
|
||||
KeyboardSelected = { Value = true },
|
||||
},
|
||||
|
||||
@@ -5,97 +5,349 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Screens;
|
||||
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.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osuTK.Input;
|
||||
using FooterButtonMods = osu.Game.Screens.SelectV2.FooterButtonMods;
|
||||
using FooterButtonOptions = osu.Game.Screens.SelectV2.FooterButtonOptions;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneSongSelect : ScreenTestScene
|
||||
public partial class TestSceneSongSelect : SongSelectTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly ScreenFooter screenScreenFooter;
|
||||
|
||||
[Cached]
|
||||
private readonly OsuLogo logo;
|
||||
|
||||
protected override bool UseOnlineAPI => true;
|
||||
|
||||
public TestSceneSongSelect()
|
||||
[Test]
|
||||
public void TestResultsScreenWhenClickingLeaderboardScore()
|
||||
{
|
||||
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,
|
||||
Child = screenScreenFooter = new ScreenFooter
|
||||
{
|
||||
OnBack = () => Stack.CurrentScreen.Exit(),
|
||||
},
|
||||
},
|
||||
logo = new OsuLogo
|
||||
{
|
||||
Alpha = 0f,
|
||||
},
|
||||
};
|
||||
Hash = Guid.NewGuid().ToString(),
|
||||
BeatmapHash = beatmapInfo.Hash,
|
||||
BeatmapInfo = beatmapInfo,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
User = new GuestUser(),
|
||||
});
|
||||
});
|
||||
|
||||
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]
|
||||
private void load()
|
||||
#region Hotkeys
|
||||
|
||||
[Test]
|
||||
public void TestDeleteHotkey()
|
||||
{
|
||||
RealmDetachedBeatmapStore beatmapStore;
|
||||
LoadSongSelect();
|
||||
|
||||
Dependencies.CacheAs<BeatmapStore>(beatmapStore = new RealmDetachedBeatmapStore());
|
||||
Add(beatmapStore);
|
||||
}
|
||||
ImportBeatmapForRuleset(0);
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
AddAssert("beatmap imported", () => Beatmaps.GetAllUsableBeatmapSets().Any(), () => Is.True);
|
||||
|
||||
Stack.ScreenPushed += updateFooter;
|
||||
Stack.ScreenExited += updateFooter;
|
||||
}
|
||||
// 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);
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
AddStep("press shift-delete", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Key(Key.Delete);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
|
||||
AddStep("load screen", () => Stack.Push(new SoloSongSelect()));
|
||||
AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded);
|
||||
AddUntilStep("delete dialog shown", () => DialogOverlay.CurrentDialog, Is.InstanceOf<BeatmapDeleteDialog>);
|
||||
AddStep("confirm deletion", () => DialogOverlay.CurrentDialog!.PerformAction<PopupDialogDangerousButton>());
|
||||
|
||||
AddAssert("beatmap set deleted", () => Beatmaps.GetAllUsableBeatmapSets().Any(), () => Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRulesets()
|
||||
public void TestClearModsViaModButtonRightClick()
|
||||
{
|
||||
AddStep("set osu ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
|
||||
AddStep("set taiko ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
|
||||
AddStep("set catch ruleset", () => Ruleset.Value = new CatchRuleset().RulesetInfo);
|
||||
AddStep("set mania ruleset", () => Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
LoadSongSelect();
|
||||
|
||||
AddStep("select NC", () => SelectedMods.Value = new[] { new OsuModNightcore() });
|
||||
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
|
||||
|
||||
[Test]
|
||||
public void TestMods()
|
||||
public void TestFooterMods()
|
||||
{
|
||||
LoadSongSelect();
|
||||
|
||||
AddStep("one mod", () => SelectedMods.Value = new List<Mod> { new OsuModHidden() });
|
||||
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() });
|
||||
@@ -121,27 +373,25 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowOptions()
|
||||
public void TestFooterModOverlay()
|
||||
{
|
||||
AddStep("enable options", () =>
|
||||
LoadSongSelect();
|
||||
|
||||
AddStep("Press F1", () =>
|
||||
{
|
||||
var optionsButton = this.ChildrenOfType<ScreenFooterButton>().Last();
|
||||
|
||||
optionsButton.Enabled.Value = true;
|
||||
optionsButton.TriggerClick();
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonMods>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestState()
|
||||
{
|
||||
AddToggleStep("set options enabled state", state => this.ChildrenOfType<ScreenFooterButton>().Last().Enabled.Value = state);
|
||||
AddAssert("Overlay visible", () => this.ChildrenOfType<ModSelectOverlay>().Single().State.Value == Visibility.Visible);
|
||||
AddStep("Hide", () => this.ChildrenOfType<ModSelectOverlay>().Single().Hide());
|
||||
}
|
||||
|
||||
// add these test cases when functionality is implemented.
|
||||
// [Test]
|
||||
// public void TestFooterRandom()
|
||||
// {
|
||||
// loadSongSelect();
|
||||
//
|
||||
// AddStep("press F2", () => InputManager.Key(Key.F2));
|
||||
// AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
|
||||
// }
|
||||
@@ -149,6 +399,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
// [Test]
|
||||
// public void TestFooterRandomViaMouse()
|
||||
// {
|
||||
// loadSongSelect();
|
||||
//
|
||||
// AddStep("click button", () =>
|
||||
// {
|
||||
// InputManager.MoveMouseTo(randomButton);
|
||||
@@ -160,6 +412,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
// [Test]
|
||||
// public void TestFooterRewind()
|
||||
// {
|
||||
// loadSongSelect();
|
||||
//
|
||||
// AddStep("press Shift+F2", () =>
|
||||
// {
|
||||
// InputManager.PressKey(Key.LShift);
|
||||
@@ -173,6 +427,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
// [Test]
|
||||
// public void TestFooterRewindViaShiftMouseLeft()
|
||||
// {
|
||||
// loadSongSelect();
|
||||
//
|
||||
// AddStep("shift + click button", () =>
|
||||
// {
|
||||
// InputManager.PressKey(Key.LShift);
|
||||
@@ -186,6 +442,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
// [Test]
|
||||
// public void TestFooterRewindViaMouseRight()
|
||||
// {
|
||||
// loadSongSelect();
|
||||
//
|
||||
// AddStep("right click button", () =>
|
||||
// {
|
||||
// InputManager.MoveMouseTo(randomButton);
|
||||
@@ -195,31 +453,46 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
// }
|
||||
|
||||
[Test]
|
||||
public void TestOverlayPresent()
|
||||
public void TestFooterOptions()
|
||||
{
|
||||
AddStep("Press F1", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonMods>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddAssert("Overlay visible", () => this.ChildrenOfType<ModSelectOverlay>().Single().State.Value == Visibility.Visible);
|
||||
AddStep("Hide", () => this.ChildrenOfType<ModSelectOverlay>().Single().Hide());
|
||||
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("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
|
||||
|
||||
private void updateFooter(IScreen? _, IScreen? newScreen)
|
||||
{
|
||||
if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter)
|
||||
{
|
||||
screenScreenFooter.Show();
|
||||
screenScreenFooter.SetButtons(osuScreen.CreateFooterButtons());
|
||||
}
|
||||
else
|
||||
{
|
||||
screenScreenFooter.Hide();
|
||||
screenScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using FilterControl = osu.Game.Screens.SelectV2.FilterControl;
|
||||
using NoResultsPlaceholder = osu.Game.Screens.SelectV2.NoResultsPlaceholder;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneSongSelectFiltering : SongSelectTestScene
|
||||
{
|
||||
private FilterControl filter => SongSelect.ChildrenOfType<FilterControl>().Single();
|
||||
private ShearedFilterTextBox filterTextBox => SongSelect.ChildrenOfType<ShearedFilterTextBox>().Single();
|
||||
private int filterOperationsCount;
|
||||
|
||||
protected override void LoadSongSelect()
|
||||
{
|
||||
base.LoadSongSelect();
|
||||
|
||||
AddStep("hook filter event", () =>
|
||||
{
|
||||
filterOperationsCount = 0;
|
||||
filter.CriteriaChanged += _ => filterOperationsCount++;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleFilterOnEnter()
|
||||
{
|
||||
ImportBeatmapForRuleset(0);
|
||||
ImportBeatmapForRuleset(0);
|
||||
|
||||
LoadSongSelect();
|
||||
|
||||
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoFilterOnSimpleResume()
|
||||
{
|
||||
ImportBeatmapForRuleset(0);
|
||||
ImportBeatmapForRuleset(0);
|
||||
|
||||
LoadSongSelect();
|
||||
|
||||
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
|
||||
WaitForSuspension();
|
||||
|
||||
AddStep("return", () => SongSelect.MakeCurrent());
|
||||
AddUntilStep("wait for current", () => SongSelect.IsCurrentScreen());
|
||||
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFilterOnResumeAfterChange()
|
||||
{
|
||||
ImportBeatmapForRuleset(0);
|
||||
ImportBeatmapForRuleset(0);
|
||||
|
||||
AddStep("change convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
|
||||
|
||||
LoadSongSelect();
|
||||
|
||||
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
|
||||
WaitForSuspension();
|
||||
|
||||
AddStep("change convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
|
||||
|
||||
AddStep("return", () => SongSelect.MakeCurrent());
|
||||
AddUntilStep("wait for current", () => SongSelect.IsCurrentScreen());
|
||||
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSorting()
|
||||
{
|
||||
LoadSongSelect();
|
||||
AddManyTestMaps();
|
||||
|
||||
// TODO: old test has this step, but there doesn't seem to be any purpose for it.
|
||||
// AddUntilStep("random map selected", () => Beatmap.Value != defaultBeatmap);
|
||||
|
||||
AddStep(@"Sort by Artist", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
|
||||
AddStep(@"Sort by Title", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
|
||||
AddStep(@"Sort by Author", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Author));
|
||||
AddStep(@"Sort by DateAdded", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.DateAdded));
|
||||
AddStep(@"Sort by BPM", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.BPM));
|
||||
AddStep(@"Sort by Length", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Length));
|
||||
AddStep(@"Sort by Difficulty", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Difficulty));
|
||||
AddStep(@"Sort by Source", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Source));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCutInFilterTextBox()
|
||||
{
|
||||
LoadSongSelect();
|
||||
|
||||
AddStep("set filter text", () => filterTextBox.Current.Value = "nonono");
|
||||
AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll));
|
||||
AddStep("press ctrl/cmd-x", () => InputManager.Keys(PlatformAction.Cut));
|
||||
|
||||
AddAssert("filter text cleared", () => filterTextBox.Current.Value, () => Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonFilterableModChange()
|
||||
{
|
||||
ImportBeatmapForRuleset(0);
|
||||
|
||||
LoadSongSelect();
|
||||
|
||||
// Mod that is guaranteed to never re-filter.
|
||||
AddStep("add non-filterable mod", () => SelectedMods.Value = new Mod[] { new OsuModCinema() });
|
||||
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||
|
||||
// Removing the mod should still not re-filter.
|
||||
AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFilterableModChange()
|
||||
{
|
||||
ImportBeatmapForRuleset(3);
|
||||
|
||||
LoadSongSelect();
|
||||
|
||||
// Change to mania ruleset.
|
||||
AddStep("filter to mania ruleset", () => Ruleset.Value = Rulesets.AvailableRulesets.First(r => r.OnlineID == 3));
|
||||
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
|
||||
|
||||
// Apply a mod, but this should NOT re-filter because there's no search text.
|
||||
AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() });
|
||||
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
|
||||
|
||||
// Set search text. Should re-filter.
|
||||
AddStep("set search text to match mods", () => filterTextBox.Current.Value = "keys=3");
|
||||
AddAssert("filter count is 2", () => filterOperationsCount, () => Is.EqualTo(2));
|
||||
|
||||
// Change filterable mod. Should re-filter.
|
||||
AddStep("change new filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey5() });
|
||||
AddAssert("filter count is 3", () => filterOperationsCount, () => Is.EqualTo(3));
|
||||
|
||||
// Add non-filterable mod. Should NOT re-filter.
|
||||
AddStep("apply non-filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail(), new ManiaModKey5() });
|
||||
AddAssert("filter count is 3", () => filterOperationsCount, () => Is.EqualTo(3));
|
||||
|
||||
// Remove filterable mod. Should re-filter.
|
||||
AddStep("remove filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail() });
|
||||
AddAssert("filter count is 4", () => filterOperationsCount, () => Is.EqualTo(4));
|
||||
|
||||
// Remove non-filterable mod. Should NOT re-filter.
|
||||
AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||
AddAssert("filter count is 4", () => filterOperationsCount, () => Is.EqualTo(4));
|
||||
|
||||
// Add filterable mod. Should re-filter.
|
||||
AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() });
|
||||
AddAssert("filter count is 5", () => filterOperationsCount, () => Is.EqualTo(5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceholderVisibleAfterDeleteAll()
|
||||
{
|
||||
LoadSongSelect();
|
||||
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||
|
||||
ImportBeatmapForRuleset(0);
|
||||
AddUntilStep("wait for placeholder hidden", () => getPlaceholder()?.State.Value == Visibility.Hidden);
|
||||
|
||||
AddStep("delete all beatmaps", () => Beatmaps.Delete());
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceholderVisibleAfterStarDifficultyFilter()
|
||||
{
|
||||
ImportBeatmapForRuleset(0);
|
||||
AddStep("change star filter", () => Config.SetValue(OsuSetting.DisplayStarsMinimum, 10.0));
|
||||
|
||||
LoadSongSelect();
|
||||
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType<DrawableLinkCompiler>().First().TriggerClick());
|
||||
|
||||
AddUntilStep("star filter reset", () => Config.Get<double>(OsuSetting.DisplayStarsMinimum) == 0.0);
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceholderVisibleWithConvertSetting()
|
||||
{
|
||||
ImportBeatmapForRuleset(0);
|
||||
AddStep("change convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
|
||||
|
||||
LoadSongSelect();
|
||||
|
||||
ChangeRuleset(2);
|
||||
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType<DrawableLinkCompiler>().First().TriggerClick());
|
||||
|
||||
AddUntilStep("convert setting changed", () => Config.Get<bool>(OsuSetting.ShowConvertedBeatmaps));
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCorrectMatchCountAfterDeleteAll()
|
||||
{
|
||||
LoadSongSelect();
|
||||
checkMatchedBeatmaps(0);
|
||||
|
||||
ImportBeatmapForRuleset(0);
|
||||
checkMatchedBeatmaps(3);
|
||||
|
||||
AddStep("delete all beatmaps", () => Beatmaps.Delete());
|
||||
checkMatchedBeatmaps(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCorrectMatchCountAfterHardDelete()
|
||||
{
|
||||
LoadSongSelect();
|
||||
checkMatchedBeatmaps(0);
|
||||
|
||||
ImportBeatmapForRuleset(0);
|
||||
checkMatchedBeatmaps(3);
|
||||
|
||||
AddStep("hard delete beatmap", () => Realm.Write(r => r.RemoveRange(r.All<BeatmapSetInfo>().Where(s => !s.Protected))));
|
||||
checkMatchedBeatmaps(0);
|
||||
}
|
||||
|
||||
private NoResultsPlaceholder? getPlaceholder() => SongSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault();
|
||||
|
||||
private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneSongSelectNavigation : OsuGameTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
AddWaitStep("wait", 5);
|
||||
PushAndConfirm(() => new Screens.SelectV2.SoloSongSelect());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClickLogo()
|
||||
{
|
||||
AddStep("click", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Game.ChildrenOfType<OsuLogo>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
59
osu.Game.Tests/Visual/TestSceneReplayStability.cs
Normal file
59
osu.Game.Tests/Visual/TestSceneReplayStability.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual
|
||||
{
|
||||
public partial class TestSceneReplayStability : ReplayStabilityTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestOutrageouslyLargeLeadInTime()
|
||||
{
|
||||
// "graciously borrowed" from https://osu.ppy.sh/beatmapsets/948643#osu/1981090
|
||||
const double lead_in_time = 2147272727;
|
||||
const double hit_circle_time = 100;
|
||||
|
||||
var beatmap = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = hit_circle_time,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2
|
||||
}
|
||||
},
|
||||
AudioLeadIn = lead_in_time,
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
},
|
||||
};
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames = Enumerable.Range(0, 300).Select(t => new OsuReplayFrame(-lead_in_time + 40 * t, new Vector2(t), t % 2 == 0 ? [] : [OsuAction.LeftButton]))
|
||||
.Concat([
|
||||
new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2),
|
||||
new OsuReplayFrame(hit_circle_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(hit_circle_time + 20, OsuPlayfield.BASE_SIZE / 2),
|
||||
])
|
||||
.Cast<ReplayFrame>()
|
||||
.ToList(),
|
||||
};
|
||||
|
||||
RunTest(beatmap, replay, [HitResult.Great]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneBackButton : OsuTestScene
|
||||
{
|
||||
private readonly BackButton? button;
|
||||
|
||||
public TestSceneBackButton()
|
||||
{
|
||||
BackButton button;
|
||||
ScreenFooter.BackReceptor receptor = new ScreenFooter.BackReceptor();
|
||||
|
||||
Child = new Container
|
||||
@@ -34,14 +35,13 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
},
|
||||
button = new BackButton(receptor)
|
||||
{
|
||||
Action = () => button?.Hide(),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
button.Action = () => button.Hide();
|
||||
|
||||
AddStep("show button", () => button.Show());
|
||||
AddStep("hide button", () => button.Hide());
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -16,7 +17,7 @@ using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneModIcon : OsuTestScene
|
||||
public partial class TestSceneModIcon : OsuManualInputManagerTestScene
|
||||
{
|
||||
private FillFlowContainer spreadOutFlow = null!;
|
||||
private ModDisplay modDisplay = null!;
|
||||
@@ -181,5 +182,40 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTooltip()
|
||||
{
|
||||
OsuModDoubleTime mod = null!;
|
||||
|
||||
AddStep("create icon", () => addRange([mod = new OsuModDoubleTime()]));
|
||||
AddStep("hover", () => InputManager.MoveMouseTo(this.ChildrenOfType<ModIcon>().First()));
|
||||
AddUntilStep("tooltip displayed", () => getTooltip()?.IsPresent, () => Is.True);
|
||||
AddAssert("tooltip text = \"Double Time\"", getTooltipText, () => Is.EqualTo("Double Time"));
|
||||
AddAssert("tooltip settings empty", () => getTooltipSettingsLabels().Concat(getTooltipSettingsValues()), () => Is.Empty);
|
||||
|
||||
AddStep("change settings", () => mod.SpeedChange.Value = 1.75f);
|
||||
AddAssert("tooltip text = \"Double Time\"", getTooltipText, () => Is.EqualTo("Double Time"));
|
||||
AddAssert("tooltip settings updated",
|
||||
() => getTooltipSettingsLabels().Concat(getTooltipSettingsValues()),
|
||||
() => Is.EquivalentTo(new[] { "Speed ", "change", "1.75x" }));
|
||||
|
||||
AddStep("change settings", () => mod.SpeedChange.Value = 1.25f);
|
||||
AddAssert("tooltip text = \"Double Time\"", getTooltipText, () => Is.EqualTo("Double Time"));
|
||||
AddAssert("tooltip settings updated",
|
||||
() => getTooltipSettingsLabels().Concat(getTooltipSettingsValues()),
|
||||
() => Is.EquivalentTo(new[] { "Speed ", "change", "1.25x" }));
|
||||
|
||||
AddStep("rest settings", () => mod.SpeedChange.SetDefault());
|
||||
AddAssert("tooltip text = \"Double Time\"", getTooltipText, () => Is.EqualTo("Double Time"));
|
||||
AddAssert("tooltip settings empty", () => getTooltipSettingsLabels().Concat(getTooltipSettingsValues()), () => Is.Empty);
|
||||
|
||||
ModTooltip? getTooltip() => this.ChildrenOfType<ModTooltip>().SingleOrDefault();
|
||||
|
||||
// we could also just expose those directly from ModTooltip, but this works.
|
||||
string getTooltipText() => getTooltip().ChildrenOfType<SpriteText>().First().Text.ToString();
|
||||
IEnumerable<string> getTooltipSettingsLabels() => getTooltip().ChildrenOfType<TextFlowContainer>().First().ChildrenOfType<SpriteText>().Select(t => t.Text.ToString());
|
||||
IEnumerable<string> getTooltipSettingsValues() => getTooltip().ChildrenOfType<TextFlowContainer>().Last().ChildrenOfType<SpriteText>().Select(t => t.Text.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Anchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f,
|
||||
FilterText = "12345 matches",
|
||||
StatusText = "12345 matches",
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -72,58 +72,66 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
first.PerformWrite(updated =>
|
||||
{
|
||||
var realm = updated.Realm;
|
||||
|
||||
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())
|
||||
try
|
||||
{
|
||||
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..
|
||||
if (beatmap.Scores.Any())
|
||||
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.Hash);
|
||||
|
||||
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)
|
||||
score.BeatmapInfo = updatedBeatmap;
|
||||
foreach (var score in beatmap.Scores)
|
||||
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;
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace osu.Game.Configuration
|
||||
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.BeatmapWithStoryboard))]
|
||||
BeatmapWithStoryboard,
|
||||
|
||||
[Description("Use 'Textures/Webm/*.webm' file from local folders")]
|
||||
[Description("Use 'Resource\\Webm\\*.webm' file from local folders")]
|
||||
WebmSource,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace osu.Game.Configuration
|
||||
{
|
||||
// UI/selection defaults
|
||||
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.BeatmapDetailModsFilter, false);
|
||||
@@ -157,7 +157,7 @@ namespace osu.Game.Configuration
|
||||
|
||||
SetDefault(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised);
|
||||
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.AccuracyCutoffS, 0.95, 0.95, 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.EditorSubmissionLoadInBrowserAfterSubmission, true);
|
||||
|
||||
SetDefault(OsuSetting.WasSupporter, false);
|
||||
}
|
||||
|
||||
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
|
||||
@@ -486,5 +488,11 @@ namespace osu.Game.Configuration
|
||||
EditorShowStoryboard,
|
||||
EditorSubmissionNotifyOnDiscussionReplies,
|
||||
EditorSubmissionLoadInBrowserAfterSubmission,
|
||||
|
||||
/// <summary>
|
||||
/// Cached state of whether local user is a supporter.
|
||||
/// Used to allow early checks (ie for startup samples) to be in the correct state, even if the API authentication process has not completed.
|
||||
/// </summary>
|
||||
WasSupporter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ using osu.Framework.Logging;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@@ -35,6 +36,11 @@ namespace osu.Game.Graphics.Carousel
|
||||
{
|
||||
#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>
|
||||
/// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it.
|
||||
/// </summary>
|
||||
@@ -68,7 +74,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
public int ItemsTracked => Items.Count;
|
||||
|
||||
/// <summary>
|
||||
/// The number of carousel items currently in rotation for display.
|
||||
/// The items currently in rotation for display.
|
||||
/// </summary>
|
||||
public int DisplayableItems => carouselItems?.Count ?? 0;
|
||||
|
||||
@@ -164,7 +170,12 @@ namespace osu.Game.Graphics.Carousel
|
||||
/// <summary>
|
||||
/// Queue an asynchronous filter operation.
|
||||
/// </summary>
|
||||
protected virtual Task FilterAsync() => filterTask = performFilter();
|
||||
protected virtual Task<IEnumerable<CarouselItem>> FilterAsync()
|
||||
{
|
||||
filterTask = performFilter();
|
||||
filterTask.FireAndForget();
|
||||
return filterTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether two models are the same for display purposes.
|
||||
@@ -246,10 +257,10 @@ namespace osu.Game.Graphics.Carousel
|
||||
|
||||
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 async Task performFilter()
|
||||
private async Task<IEnumerable<CarouselItem>> performFilter()
|
||||
{
|
||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||
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).
|
||||
// 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 () =>
|
||||
{
|
||||
@@ -275,6 +286,11 @@ namespace osu.Game.Graphics.Carousel
|
||||
{
|
||||
log($"Performing {filter.GetType().ReadableName()}");
|
||||
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");
|
||||
@@ -287,20 +303,26 @@ namespace osu.Game.Graphics.Carousel
|
||||
}, cts.Token).ConfigureAwait(false);
|
||||
|
||||
if (cts.Token.IsCancellationRequested)
|
||||
return;
|
||||
return Enumerable.Empty<CarouselItem>();
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
log("Items ready for display");
|
||||
carouselItems = items.ToList();
|
||||
carouselItems = items;
|
||||
displayedRange = null;
|
||||
|
||||
// Need to call this to ensure correct post-selection logic is handled on the new items list.
|
||||
HandleItemSelected(currentSelection.Model);
|
||||
|
||||
refreshAfterSelection();
|
||||
if (!Scroll.UserScrolling)
|
||||
scrollToSelection();
|
||||
|
||||
NewItemsPresented?.Invoke();
|
||||
});
|
||||
|
||||
return items;
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.Select:
|
||||
if (currentKeyboardSelection.CarouselItem != null)
|
||||
Activate(currentKeyboardSelection.CarouselItem);
|
||||
activateSelection();
|
||||
return true;
|
||||
|
||||
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)
|
||||
{
|
||||
if (carouselItems == null || carouselItems.Count == 0) return;
|
||||
@@ -451,6 +493,9 @@ namespace osu.Game.Graphics.Carousel
|
||||
|
||||
#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 Selection currentKeyboardSelection = new Selection();
|
||||
@@ -551,7 +596,10 @@ namespace osu.Game.Graphics.Carousel
|
||||
if (!selectionValid.IsValid)
|
||||
{
|
||||
refreshAfterSelection();
|
||||
|
||||
// Always scroll to selection in this case (regardless of `UserScrolling` state), centering the selection.
|
||||
scrollToSelection();
|
||||
|
||||
selectionValid.Validate();
|
||||
}
|
||||
|
||||
@@ -575,7 +623,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
if (c.Item == null)
|
||||
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);
|
||||
|
||||
if (c.DrawYPosition != c.Item.CarouselYPosition)
|
||||
@@ -647,6 +695,12 @@ namespace osu.Game.Graphics.Carousel
|
||||
{
|
||||
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.
|
||||
// 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.
|
||||
expirePanelImmediately(panel);
|
||||
expirePanel(panel);
|
||||
}
|
||||
|
||||
// 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)
|
||||
throw new InvalidOperationException($"Carousel panel drawables must implement {typeof(ICarouselPanel)}");
|
||||
|
||||
carouselPanel.DrawYPosition = item.CarouselYPosition;
|
||||
carouselPanel.Item = item;
|
||||
|
||||
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
|
||||
// most items are not displayed / loaded).
|
||||
if (carouselItems.Count > 0)
|
||||
@@ -690,13 +768,18 @@ namespace osu.Game.Graphics.Carousel
|
||||
Scroll.SetLayoutHeight(0);
|
||||
}
|
||||
|
||||
private static void expirePanelImmediately(Drawable panel)
|
||||
private void expirePanel(Drawable panel)
|
||||
{
|
||||
panel.FinishTransforms();
|
||||
panel.Expire();
|
||||
|
||||
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.Selected.Value = false;
|
||||
carouselPanel.KeyboardSelected.Value = false;
|
||||
@@ -753,12 +836,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
base.OffsetScrollPosition(offset);
|
||||
|
||||
foreach (var panel in Panels)
|
||||
{
|
||||
var c = (ICarouselPanel)panel;
|
||||
Debug.Assert(c.Item != null);
|
||||
|
||||
c.DrawYPosition += offset;
|
||||
}
|
||||
((ICarouselPanel)panel).DrawYPosition += offset;
|
||||
}
|
||||
|
||||
public override void Clear(bool disposeChildren)
|
||||
|
||||
@@ -18,6 +18,6 @@ namespace osu.Game.Graphics.Carousel
|
||||
/// <param name="items">The items to be filtered.</param>
|
||||
/// <param name="cancellationToken">A cancellation token.</param>
|
||||
/// <returns>The post-filtered items.</returns>
|
||||
Task<IEnumerable<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken);
|
||||
Task<List<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Extensions.MatrixExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Layout;
|
||||
@@ -54,7 +55,8 @@ namespace osu.Game.Graphics.Containers
|
||||
parentMatrix.M31 = 0.0f;
|
||||
parentMatrix.M32 = 0.0f;
|
||||
|
||||
Matrix3 reversedParent = parentMatrix.Inverted();
|
||||
Matrix3 reversedParent = parentMatrix;
|
||||
MatrixExtensions.FastInvert(ref reversedParent);
|
||||
|
||||
// Extract the rotation.
|
||||
float angle = MathF.Atan2(reversedParent.M12, reversedParent.M11);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
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.
|
||||
public partial class BackButton : VisibilityContainer
|
||||
{
|
||||
public Action Action;
|
||||
public Action? Action { get; init; }
|
||||
|
||||
private readonly TwoLayerButton button;
|
||||
|
||||
public BackButton(ScreenFooter.BackReceptor receptor = null)
|
||||
public BackButton(ScreenFooter.BackReceptor? receptor = null)
|
||||
{
|
||||
Size = TwoLayerButton.SIZE_EXTENDED;
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
private const float filter_text_size = 12;
|
||||
|
||||
public LocalisableString FilterText
|
||||
public LocalisableString StatusText
|
||||
{
|
||||
get => ((InnerFilterTextBox)TextBox).FilterText.Text;
|
||||
set => Schedule(() => ((InnerFilterTextBox)TextBox).FilterText.Text = value);
|
||||
get => ((InnerFilterTextBox)TextBox).StatusText.Text;
|
||||
set => Schedule(() => ((InnerFilterTextBox)TextBox).StatusText.Text = value);
|
||||
}
|
||||
|
||||
public ShearedFilterTextBox()
|
||||
@@ -27,12 +27,12 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
protected partial class InnerFilterTextBox : InnerSearchTextBox
|
||||
{
|
||||
public OsuSpriteText FilterText { get; private set; } = null!;
|
||||
public OsuSpriteText StatusText { get; private set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
TextContainer.Add(FilterText = new OsuSpriteText
|
||||
TextContainer.Add(StatusText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
|
||||
@@ -135,6 +135,11 @@ Click to see what's new!", version);
|
||||
/// </summary>
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,8 @@ namespace osu.Game.Online.API
|
||||
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
|
||||
|
||||
private readonly Bindable<UserStatus> configStatus = new Bindable<UserStatus>();
|
||||
private readonly Bindable<bool> configSupporter = new Bindable<bool>();
|
||||
|
||||
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
|
||||
private readonly Logger log;
|
||||
|
||||
@@ -104,6 +106,7 @@ namespace osu.Game.Online.API
|
||||
authentication.Token.ValueChanged += onTokenChanged;
|
||||
|
||||
config.BindWith(OsuSetting.UserOnlineStatus, configStatus);
|
||||
config.BindWith(OsuSetting.WasSupporter, configSupporter);
|
||||
|
||||
if (HasLogin)
|
||||
{
|
||||
@@ -333,6 +336,7 @@ namespace osu.Game.Online.API
|
||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||
|
||||
localUser.Value = me;
|
||||
configSupporter.Value = me.IsSupporter;
|
||||
state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth;
|
||||
failureCount = 0;
|
||||
};
|
||||
@@ -368,7 +372,8 @@ namespace osu.Game.Online.API
|
||||
|
||||
localUser.Value = new APIUser
|
||||
{
|
||||
Username = ProvidedUsername
|
||||
Username = ProvidedUsername,
|
||||
IsSupporter = configSupporter.Value,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -607,6 +612,7 @@ namespace osu.Game.Online.API
|
||||
Schedule(() =>
|
||||
{
|
||||
localUser.Value = createGuestUser();
|
||||
configSupporter.Value = false;
|
||||
friends.Clear();
|
||||
});
|
||||
|
||||
|
||||
@@ -7,15 +7,18 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Game.Extensions;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
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 BeatmapLeaderboardScope scope;
|
||||
@@ -36,19 +39,20 @@ namespace osu.Game.Online.API.Requests
|
||||
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()}");
|
||||
query.Append($@"&mode={ruleset.ShortName}");
|
||||
req.AddParameter(@"type", scope.ToString().ToLowerInvariant());
|
||||
req.AddParameter(@"mode", ruleset.ShortName);
|
||||
|
||||
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)
|
||||
|
||||
@@ -129,8 +129,8 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
switch (args[0])
|
||||
{
|
||||
case "http":
|
||||
case "https":
|
||||
case @"http":
|
||||
case @"https":
|
||||
// length > 3 since all these links need another argument to work
|
||||
if (args.Length > 3 && args[1].EndsWith(WebsiteRootUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -139,8 +139,8 @@ namespace osu.Game.Online.Chat
|
||||
switch (args[2])
|
||||
{
|
||||
// old site only
|
||||
case "b":
|
||||
case "beatmaps":
|
||||
case @"b":
|
||||
case @"beatmaps":
|
||||
{
|
||||
string trimmed = mainArg.Split('?').First();
|
||||
if (int.TryParse(trimmed, out int id))
|
||||
@@ -149,11 +149,11 @@ namespace osu.Game.Online.Chat
|
||||
break;
|
||||
}
|
||||
|
||||
case "s":
|
||||
case "beatmapsets":
|
||||
case "d":
|
||||
case @"s":
|
||||
case @"beatmapsets":
|
||||
case @"d":
|
||||
{
|
||||
if (mainArg == "discussions")
|
||||
if (mainArg == @"discussions")
|
||||
// handle discussion links externally for now
|
||||
return new LinkDetails(LinkAction.External, url);
|
||||
|
||||
@@ -169,15 +169,15 @@ namespace osu.Game.Online.Chat
|
||||
break;
|
||||
}
|
||||
|
||||
case "u":
|
||||
case "users":
|
||||
case @"u":
|
||||
case @"users":
|
||||
return getUserLink(mainArg);
|
||||
|
||||
case "wiki":
|
||||
case @"wiki":
|
||||
return new LinkDetails(LinkAction.OpenWiki, string.Join('/', args.Skip(3)));
|
||||
|
||||
case "home":
|
||||
if (mainArg != "changelog")
|
||||
case @"home":
|
||||
if (mainArg != @"changelog")
|
||||
// handle link other than changelog as external for now
|
||||
return new LinkDetails(LinkAction.External, url);
|
||||
|
||||
@@ -192,13 +192,26 @@ namespace osu.Game.Online.Chat
|
||||
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;
|
||||
|
||||
case "osu":
|
||||
case @"osu":
|
||||
// every internal link also needs some kind of argument
|
||||
if (args.Length < 3)
|
||||
break;
|
||||
@@ -207,38 +220,39 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
switch (args[1])
|
||||
{
|
||||
case "chan":
|
||||
case @"chan":
|
||||
linkType = LinkAction.OpenChannel;
|
||||
break;
|
||||
|
||||
case "edit":
|
||||
case @"edit":
|
||||
linkType = LinkAction.OpenEditorTimestamp;
|
||||
break;
|
||||
|
||||
case "b":
|
||||
case @"b":
|
||||
linkType = LinkAction.OpenBeatmap;
|
||||
break;
|
||||
|
||||
case "s":
|
||||
case "dl":
|
||||
case @"s":
|
||||
case @"dl":
|
||||
linkType = LinkAction.OpenBeatmapSet;
|
||||
break;
|
||||
|
||||
case "spectate":
|
||||
case @"spectate":
|
||||
linkType = LinkAction.Spectate;
|
||||
break;
|
||||
|
||||
case "u":
|
||||
case @"u":
|
||||
return getUserLink(args[2]);
|
||||
|
||||
case @"room":
|
||||
linkType = LinkAction.JoinRoom;
|
||||
break;
|
||||
|
||||
default:
|
||||
return new LinkDetails(LinkAction.External, url);
|
||||
}
|
||||
|
||||
return new LinkDetails(linkType, HttpUtility.UrlDecode(args[2]));
|
||||
|
||||
case "osump":
|
||||
return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]);
|
||||
}
|
||||
|
||||
return new LinkDetails(LinkAction.External, url);
|
||||
@@ -337,7 +351,7 @@ namespace osu.Game.Online.Chat
|
||||
OpenBeatmapSet,
|
||||
OpenChannel,
|
||||
OpenEditorTimestamp,
|
||||
JoinMultiplayerMatch,
|
||||
JoinRoom,
|
||||
Spectate,
|
||||
OpenUserProfile,
|
||||
SearchBeatmapSet,
|
||||
|
||||
@@ -67,6 +67,7 @@ using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select;
|
||||
@@ -79,6 +80,7 @@ using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using Sentry;
|
||||
using MatchType = osu.Game.Online.Rooms.MatchType;
|
||||
|
||||
namespace osu.Game
|
||||
{
|
||||
@@ -186,7 +188,7 @@ namespace osu.Game
|
||||
/// <summary>
|
||||
/// Whether the back button is currently displayed.
|
||||
/// </summary>
|
||||
private readonly IBindable<bool> backButtonVisibility = new Bindable<bool>();
|
||||
private readonly IBindable<bool> backButtonVisibility = new BindableBool();
|
||||
|
||||
IBindable<LocalUserPlayingState> ILocalUserPlayInfo.PlayingState => UserPlayingState;
|
||||
|
||||
@@ -491,7 +493,6 @@ namespace osu.Game
|
||||
HandleTimestamp(argString);
|
||||
break;
|
||||
|
||||
case LinkAction.JoinMultiplayerMatch:
|
||||
case LinkAction.Spectate:
|
||||
waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification
|
||||
{
|
||||
@@ -523,6 +524,11 @@ namespace osu.Game
|
||||
|
||||
break;
|
||||
|
||||
case LinkAction.JoinRoom:
|
||||
if (long.TryParse(argString, out long roomId))
|
||||
JoinRoom(roomId);
|
||||
break;
|
||||
|
||||
default:
|
||||
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>
|
||||
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>
|
||||
/// 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).
|
||||
@@ -725,6 +753,22 @@ namespace osu.Game
|
||||
/// <param name="password">The password to join the room, if any is given.</param>
|
||||
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 =>
|
||||
{
|
||||
if (!(screen is Multiplayer multiplayer))
|
||||
@@ -736,6 +780,23 @@ namespace osu.Game
|
||||
// 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>
|
||||
/// Present a score's replay immediately.
|
||||
/// The user should have already requested this interactively.
|
||||
@@ -1044,7 +1105,7 @@ namespace osu.Game
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Action = () => ScreenFooter.OnBack?.Invoke(),
|
||||
Action = handleBackButton,
|
||||
},
|
||||
logoContainer = new Container { RelativeSizeAxes = Axes.Both },
|
||||
footerBasedOverlayContent = new Container
|
||||
@@ -1059,14 +1120,7 @@ namespace osu.Game
|
||||
Child = ScreenFooter = new ScreenFooter(backReceptor)
|
||||
{
|
||||
RequestLogoInFront = inFront => ScreenContainer.ChangeChildDepth(logoContainer, inFront ? float.MinValue : 0),
|
||||
OnBack = () =>
|
||||
{
|
||||
if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen))
|
||||
return;
|
||||
|
||||
if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowUserExit && !currentScreen.OnBackButton()))
|
||||
ScreenStack.Exit();
|
||||
}
|
||||
BackButtonPressed = handleBackButton
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1247,6 +1301,13 @@ namespace osu.Game
|
||||
handleStartupImport();
|
||||
}
|
||||
|
||||
private void handleBackButton()
|
||||
{
|
||||
if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen)) return;
|
||||
|
||||
if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowUserExit && !currentScreen.OnBackButton())) ScreenStack.Exit();
|
||||
}
|
||||
|
||||
private void handleStartupImport()
|
||||
{
|
||||
if (args?.Length > 0)
|
||||
@@ -1657,7 +1718,6 @@ namespace osu.Game
|
||||
// Bind to new screen.
|
||||
if (newScreen != null)
|
||||
{
|
||||
backButtonVisibility.BindTo(newScreen.BackButtonVisibility);
|
||||
OverlayActivationMode.BindTo(newScreen.OverlayActivationMode);
|
||||
configUserActivity.BindTo(newScreen.Activity);
|
||||
|
||||
@@ -1669,19 +1729,36 @@ namespace osu.Game
|
||||
else
|
||||
Toolbar.Show();
|
||||
|
||||
var newOsuScreen = (OsuScreen)newScreen;
|
||||
|
||||
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();
|
||||
ScreenFooter.SetButtons(newScreen.CreateFooterButtons());
|
||||
ScreenFooter.Show();
|
||||
|
||||
newOsuScreen.OnLoadComplete += _ =>
|
||||
{
|
||||
var buttons = newScreen.CreateFooterButtons();
|
||||
|
||||
newOsuScreen.LoadComponentsAgainstScreenDependencies(buttons);
|
||||
|
||||
ScreenFooter.SetButtons(buttons);
|
||||
ScreenFooter.Show();
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
backButtonVisibility.BindTo(newScreen.BackButtonVisibility);
|
||||
|
||||
ScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
|
||||
ScreenFooter.Hide();
|
||||
}
|
||||
|
||||
skinEditor.SetTarget((OsuScreen)newScreen);
|
||||
skinEditor.SetTarget(newOsuScreen);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
@@ -9,10 +8,10 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
{
|
||||
public enum MetadataType
|
||||
{
|
||||
[Description("User Tags")] // TODO: use translated string after osu-resources update
|
||||
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoUserTags))]
|
||||
UserTags,
|
||||
|
||||
[Description("Mapper Tags")] // TODO: use translated string after osu-resources update
|
||||
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoMapperTags))]
|
||||
MapperTags,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoSource))]
|
||||
|
||||
@@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
|
||||
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);
|
||||
|
||||
api.Queue(retrievalRequest);
|
||||
@@ -124,8 +124,6 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
|
||||
protected virtual void UpdateItems(List<TModel> items, CancellationTokenSource cancellationTokenSource) => Schedule(() =>
|
||||
{
|
||||
OnItemsReceived(items);
|
||||
|
||||
if (!items.Any() && CurrentPage?.Offset == 0)
|
||||
{
|
||||
moreButton.Hide();
|
||||
@@ -137,11 +135,18 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
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 =>
|
||||
{
|
||||
missing.Hide();
|
||||
|
||||
moreButton.FadeTo(items.Count == CurrentPage?.Limit ? 1 : 0);
|
||||
moreButton.FadeTo(hasMore ? 1 : 0);
|
||||
moreButton.IsLoading = false;
|
||||
|
||||
ItemsContainer.AddRange(drawables);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@@ -31,6 +32,18 @@ namespace osu.Game.Rulesets.Mods
|
||||
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
|
||||
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;
|
||||
|
||||
protected ModDoubleTime()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@@ -31,6 +32,18 @@ namespace osu.Game.Rulesets.Mods
|
||||
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
|
||||
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;
|
||||
|
||||
protected ModHalfTime()
|
||||
|
||||
@@ -42,6 +42,9 @@ namespace osu.Game.Rulesets.Mods
|
||||
get
|
||||
{
|
||||
yield return ("Speed change", $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x");
|
||||
|
||||
if (!AdjustPitch.IsDefault)
|
||||
yield return ("Adjust pitch", AdjustPitch.Value ? "On" : "Off");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -206,6 +206,9 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <returns>One half of the hit window for <paramref name="result"/>.</returns>
|
||||
public double WindowFor(HitResult result)
|
||||
{
|
||||
if (!IsHitResultAllowed(result))
|
||||
throw new ArgumentOutOfRangeException(nameof(result), result, $@"{result} is not an allowed result.");
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
@@ -227,7 +230,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
return miss;
|
||||
|
||||
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),
|
||||
};
|
||||
|
||||
public override bool IsHitResultAllowed(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
case HitResult.Miss:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
public override bool IsHitResultAllowed(HitResult result) => true;
|
||||
|
||||
protected override DifficultyRange[] GetRanges() => ranges;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
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)
|
||||
{
|
||||
if (content == displayedContent)
|
||||
return;
|
||||
|
||||
displayedContent = content;
|
||||
nameText.Text = content.Name;
|
||||
settingsLabelsFlow.Clear();
|
||||
settingsValuesFlow.Clear();
|
||||
|
||||
if (content.SettingDescription.Any())
|
||||
if (displayedSettings == null || !displayedSettings.SequenceEqual(content.SettingDescription))
|
||||
{
|
||||
settingsLabelsFlow.Show();
|
||||
settingsValuesFlow.Show();
|
||||
displayedSettings = content.SettingDescription.ToArray();
|
||||
|
||||
foreach (var part in content.SettingDescription)
|
||||
settingsLabelsFlow.Clear();
|
||||
settingsValuesFlow.Clear();
|
||||
|
||||
if (displayedSettings.Any())
|
||||
{
|
||||
settingsLabelsFlow.AddText(part.setting);
|
||||
settingsLabelsFlow.NewLine();
|
||||
settingsLabelsFlow.Show();
|
||||
settingsValuesFlow.Show();
|
||||
|
||||
settingsValuesFlow.AddText(part.value);
|
||||
settingsValuesFlow.NewLine();
|
||||
foreach (var part in displayedSettings)
|
||||
{
|
||||
settingsLabelsFlow.AddText(part.setting);
|
||||
settingsLabelsFlow.NewLine();
|
||||
|
||||
settingsValuesFlow.AddText(part.value);
|
||||
settingsValuesFlow.NewLine();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
settingsLabelsFlow.Hide();
|
||||
settingsValuesFlow.Hide();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
settingsLabelsFlow.Hide();
|
||||
settingsValuesFlow.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,6 @@ namespace osu.Game.Rulesets.UI
|
||||
this.target = target;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Depth = float.MinValue;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace osu.Game.Scoring.Legacy
|
||||
private IBeatmap currentBeatmap;
|
||||
private Ruleset currentRuleset;
|
||||
|
||||
private float beatmapOffset;
|
||||
private long beatmapOffset;
|
||||
|
||||
public Score Parse(Stream stream)
|
||||
{
|
||||
@@ -262,7 +262,7 @@ namespace osu.Game.Scoring.Legacy
|
||||
|
||||
private void readLegacyReplay(Replay replay, StreamReader reader)
|
||||
{
|
||||
float lastTime = beatmapOffset;
|
||||
long lastTime = beatmapOffset;
|
||||
var legacyFrames = new List<LegacyReplayFrame>();
|
||||
|
||||
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
|
||||
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 mouseY = Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE);
|
||||
|
||||
|
||||
@@ -108,12 +108,14 @@ namespace osu.Game.Screens.Backgrounds
|
||||
if (Background != null)
|
||||
{
|
||||
newDepth = Background.Depth + 1;
|
||||
Background.FinishTransforms();
|
||||
Background.FadeOut(250);
|
||||
Background.Expire();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ namespace osu.Game.Screens.Backgrounds
|
||||
|
||||
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 Background(getBackgroundTextureName());
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace osu.Game.Screens.Edit.Submission
|
||||
}
|
||||
|
||||
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)
|
||||
throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!");
|
||||
|
||||
@@ -23,12 +23,24 @@ namespace osu.Game.Screens.Footer
|
||||
{
|
||||
public partial class ScreenFooter : OverlayContainer
|
||||
{
|
||||
public 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 float delay_per_button = 30;
|
||||
private const double transition_duration = 400;
|
||||
|
||||
public const int HEIGHT = 50;
|
||||
|
||||
private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
|
||||
|
||||
private Box background = null!;
|
||||
@@ -40,15 +52,6 @@ namespace osu.Game.Screens.Footer
|
||||
[Cached]
|
||||
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)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
@@ -144,8 +147,7 @@ namespace osu.Game.Screens.Footer
|
||||
{
|
||||
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()
|
||||
@@ -326,7 +328,7 @@ namespace osu.Game.Screens.Footer
|
||||
return;
|
||||
}
|
||||
|
||||
OnBack?.Invoke();
|
||||
BackButtonPressed?.Invoke();
|
||||
}
|
||||
|
||||
public partial class BackReceptor : Drawable, IKeyBindingHandler<GlobalAction>
|
||||
|
||||
@@ -79,7 +79,9 @@ namespace osu.Game.Screens
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
// todo: make this work with footer.
|
||||
IBindable<bool> BackButtonVisibility { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -97,7 +99,7 @@ namespace osu.Game.Screens
|
||||
Bindable<RulesetInfo> Ruleset { get; }
|
||||
|
||||
/// <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>
|
||||
IReadOnlyList<ScreenFooterButton> CreateFooterButtons();
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ using osu.Game.Seasonal;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
|
||||
namespace osu.Game.Screens.Menu
|
||||
{
|
||||
@@ -239,7 +240,13 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -38,8 +38,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
public GameplayChatDisplay(Room room)
|
||||
: base(room, leaveChannelOnDispose: false)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Background.Alpha = 0.2f;
|
||||
Width = width;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
||||
@@ -8,6 +8,8 @@ using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@@ -18,6 +20,7 @@ using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
@@ -25,8 +28,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
protected override bool ShowLeaderboard => true;
|
||||
|
||||
protected override UserActivity InitialActivity => new UserActivity.InMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
|
||||
|
||||
[Resolved]
|
||||
@@ -42,6 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
private readonly MultiplayerLeaderboardProvider leaderboardProvider;
|
||||
|
||||
private GameplayMatchScoreDisplay teamScoreDisplay = null!;
|
||||
private GameplayChatDisplay chat = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a multiplayer player.
|
||||
@@ -57,7 +59,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
AllowFailAnimation = false,
|
||||
AllowSkipping = room.AutoSkip,
|
||||
AutomaticallySkipIntro = room.AutoSkip,
|
||||
AlwaysShowLeaderboard = true,
|
||||
ShowLeaderboard = true,
|
||||
})
|
||||
{
|
||||
leaderboardProvider = new MultiplayerLeaderboardProvider(users);
|
||||
@@ -71,16 +73,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
ScoreProcessor.ApplyNewJudgementsWhenFailed = true;
|
||||
|
||||
LoadComponentAsync(new GameplayChatDisplay(Room)
|
||||
LoadComponentAsync(new FillFlowContainer
|
||||
{
|
||||
Expanded = { BindTarget = LeaderboardExpandedState },
|
||||
}, chat => HUDOverlay.LeaderboardFlow.Insert(2, chat));
|
||||
Width = 260,
|
||||
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 =>
|
||||
{
|
||||
AddInternal(loaded);
|
||||
@@ -124,6 +137,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
failAndBail();
|
||||
}
|
||||
}), true);
|
||||
|
||||
LocalUserPlaying.BindValueChanged(_ => chat.Expanded.Value = !LocalUserPlaying.Value, true);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user