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

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

View File

View File

@@ -25,7 +25,3 @@ M:TagLib.File.Create(System.String);TagLib's MIME type detection changes behavio
M:TagLib.File.Create(TagLib.File.IFileAbstraction);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
M:TagLib.File.Create(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.

View File

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

View File

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

View File

@@ -0,0 +1,75 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests
{
public partial class TestSceneReplayRecording : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[Resolved]
private AudioManager audioManager { get; set; } = null!;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects =
{
new Fruit { StartTime = 0, },
new Fruit { StartTime = 5000, },
new Fruit { StartTime = 10000, },
new Fruit { StartTime = 15000, }
}
};
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[Test]
public void TestRecording()
{
seekTo(0);
AddStep("start moving left", () => InputManager.PressKey(Key.Left));
seekTo(5000);
AddStep("end moving left", () => InputManager.ReleaseKey(Key.Left));
AddAssert("catcher max left", () => this.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(0));
AddAssert("movement to left recorded to replay", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => f.Actions.SequenceEqual([CatchAction.MoveLeft])));
AddAssert("replay reached left edge", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => Precision.AlmostEquals(f.Position, 0)));
AddStep("start dashing right", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.PressKey(Key.Right);
});
seekTo(10000);
AddStep("end dashing right", () =>
{
InputManager.ReleaseKey(Key.LShift);
InputManager.ReleaseKey(Key.Right);
});
AddAssert("catcher max right", () => this.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(CatchPlayfield.WIDTH));
AddAssert("dash to right recorded to replay", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => f.Actions.SequenceEqual([CatchAction.Dash, CatchAction.MoveRight])));
AddAssert("replay reached right edge", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => Precision.AlmostEquals(f.Position, CatchPlayfield.WIDTH)));
}
private void seekTo(double time)
{
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
}
}
}

View File

@@ -49,6 +49,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
var 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(),
}
};
}

View File

@@ -31,9 +31,12 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss);
[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)

View File

@@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests
{
public partial class TestSceneReplayRecording : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Resolved]
private AudioManager audioManager { get; set; } = null!;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new ManiaBeatmap(new StageDefinition(1))
{
HitObjects =
{
new Note { StartTime = 0, },
new Note { StartTime = 5000, },
new Note { StartTime = 10000, },
new Note { StartTime = 15000, }
},
Difficulty = { CircleSize = 1 },
BeatmapInfo =
{
Ruleset = ruleset,
}
};
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[Test]
public void TestRecording()
{
seekTo(0);
AddStep("press space", () => InputManager.Key(Key.Space));
AddAssert("button press recorded to replay", () => Player.Score.Replay.Frames.OfType<ManiaReplayFrame>().Any(f => f.Actions.SequenceEqual([ManiaAction.Key1])));
}
private void seekTo(double time)
{
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
}
}
}

View File

@@ -32,6 +32,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
SetDefault(ManiaRulesetSetting.ScrollStyle, ManiaScrollingStyle.ScrollTimeStyleFixed);
SetDefault(ManiaRulesetSetting.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,

View File

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

View File

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

View File

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

View File

@@ -1,233 +0,0 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
{
public partial class EzColumnHitErrorMeter : HitErrorMeter
{
[SettingSource("Icon Height", "Icon Height")]
public BindableNumber<float> IconHeight { get; } = new BindableNumber<float>(2)
{
MinValue = 1,
MaxValue = 20,
Precision = 1f,
};
[SettingSource("Move Height", "Move Height")]
public BindableNumber<float> MoveHeight { get; } = new BindableNumber<float>(10)
{
MinValue = 1,
MaxValue = 50,
Precision = 1f,
};
[SettingSource("Background Alpha", "Background Alpha")]
public BindableNumber<float> BackgroundAlpha { get; } = new BindableNumber<float>(0.2f)
{
MinValue = 0,
MaxValue = 1,
Precision = 0.1f,
};
private double[] floatingAverages = null!;
private Box[] judgementMarkers = null!;
private Container[] columns = null!;
private float keyCount;
private OsuConfigManager config = null!;
[Resolved]
private InputCountController controller { get; set; } = null!;
public EzColumnHitErrorMeter()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
this.config = config;
recreateComponents();
}
private void recreateComponents()
{
keyCount = controller.Triggers.Count;
floatingAverages = new double[(int)keyCount];
judgementMarkers = new Box[(int)keyCount];
InternalChild = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding(2),
Children = new Drawable[]
{
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(0, 0),
Children = columns = Enumerable.Range(0, (int)keyCount).Select(index =>
{
var column = createColumn();
var marker = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.Y,
Blending = BlendingParameters.Additive,
Width = (float)config.Get<double>(OsuSetting.ColumnWidth),
Height = IconHeight.Value,
Colour = Colour4.Gray,
Alpha = 0.8f
};
column.Add(marker);
judgementMarkers[index] = marker;
return column;
}).ToArray()
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
controller.Triggers.BindCollectionChanged((_, __) => recreateComponents(), true);
// 更新标识块高度
IconHeight.BindValueChanged(height =>
{
foreach (var marker in judgementMarkers)
marker.Height = height.NewValue;
}, true);
// 更新背景柱状列高度和标识块移动范围
MoveHeight.BindValueChanged(height =>
{
foreach (var column in columns)
{
var backgroundBox = column.Children.OfType<Box>().FirstOrDefault();
if (backgroundBox != null)
{
backgroundBox.Height = height.NewValue;
}
}
foreach (var marker in judgementMarkers)
{
marker.Y = Math.Clamp(marker.Y, -height.NewValue / 2, height.NewValue / 2);
// 重新计算标识块的相对位置
float currentAbsoluteY = marker.Y * height.OldValue;
float newRelativeY = currentAbsoluteY / height.NewValue;
marker.Y = newRelativeY;
// 更新标识块的移动范围
marker.MoveToY(newRelativeY, 800, Easing.OutQuint);
}
Invalidate(Invalidation.DrawSize);
}, true);
// 更新背景透明度
BackgroundAlpha.BindValueChanged(alpha =>
{
foreach (var column in columns)
{
var backgroundBox = column.Children.OfType<Box>().FirstOrDefault();
if (backgroundBox != null)
backgroundBox.Alpha = alpha.NewValue;
}
}, true);
}
private Container createColumn()
{
var backgroundBox = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = MoveHeight.Value,
Width = (float)config.Get<double>(OsuSetting.ColumnWidth),
Colour = Colour4.Gray,
Alpha = 0.2f
};
return new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
backgroundBox
}
};
}
protected override void OnNewJudgement(JudgementResult judgement)
{
if (!judgement.IsHit || !judgement.Type.IsScorable())
return;
int columnIndex = getColumnIndex(judgement);
if (columnIndex < 0 || columnIndex >= keyCount)
return;
floatingAverages[columnIndex] = floatingAverages[columnIndex] * 0.9 + judgement.TimeOffset * 0.1;
const int marker_move_duration = 800;
var marker = judgementMarkers[columnIndex];
float targetY = getRelativeJudgementPosition(floatingAverages[columnIndex]);
marker.Y = targetY;
marker.MoveToY(targetY, marker_move_duration, Easing.OutQuint);
marker.Colour = GetColourForHitResult(judgement.Type);
}
private float getRelativeJudgementPosition(double value)
{
float moveRange = MoveHeight.Value;
return (float)Math.Clamp((value / HitWindows.WindowFor(HitResult.Miss)) * moveRange, -moveRange, moveRange);
}
private int getColumnIndex(JudgementResult judgement)
{
if (judgement.HitObject is IHasColumn hasColumn)
return hasColumn.Column;
return -1;
}
public override void Clear()
{
foreach (var column in columns)
{
column.Clear();
}
}
}
}

View File

@@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
{
public partial class EzComComboCounter : ComboCounter
{
[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;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -128,13 +128,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
keyCounter.Position = new Vector2(0, -Stage.HIT_TARGET_POSITION - stage_padding_bottom);
}
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(),

View File

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

View File

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

View File

@@ -0,0 +1,81 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneReplayRecording : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
[Resolved]
private AudioManager audioManager { get; set; } = null!;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects =
{
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 0,
},
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 5000,
},
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 10000,
},
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 15000,
}
}
};
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[Test]
public void TestRecording()
{
seekTo(0);
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
AddStep("press X", () => InputManager.Key(Key.X));
AddAssert("right button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.RightButton])));
seekTo(5000);
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
AddStep("press Z", () => InputManager.Key(Key.Z));
AddAssert("left button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.LeftButton])));
seekTo(10000);
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
AddStep("press C", () => InputManager.Key(Key.C));
AddAssert("smoke button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.Smoke])));
}
private void seekTo(double time)
{
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
}
}
}

View File

@@ -275,6 +275,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
else
{
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);

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,152 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
public partial class TestSceneTaikoModSimplifiedRhythm : TaikoModTestScene
{
[Test]
public void TestOneThirdConversion()
{
CreateModTest(new ModTestData
{
Mod = new TaikoModSimplifiedRhythm
{
OneThirdConversion = { Value = true },
},
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject>
{
new Hit { StartTime = 1000, Type = HitType.Centre },
new Hit { StartTime = 1500, Type = HitType.Centre },
new Hit { StartTime = 2000, Type = HitType.Centre },
new Hit { StartTime = 2333, Type = HitType.Rim }, // mod removes this
new Hit { StartTime = 2666, Type = HitType.Centre }, // mod moves this to 2500
new Hit { StartTime = 3000, Type = HitType.Centre },
new Hit { StartTime = 3500, Type = HitType.Centre },
},
},
ReplayFrames = new List<ReplayFrame>
{
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1200),
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
new TaikoReplayFrame(1700),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2200),
new TaikoReplayFrame(2500, TaikoAction.LeftCentre),
new TaikoReplayFrame(2700),
new TaikoReplayFrame(3000, TaikoAction.LeftCentre),
new TaikoReplayFrame(3200),
new TaikoReplayFrame(3500, TaikoAction.LeftCentre),
new TaikoReplayFrame(3700),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 6 && Player.ScoreProcessor.Accuracy.Value == 1
});
}
[Test]
public void TestOneSixthConversion() => CreateModTest(new ModTestData
{
Mod = new TaikoModSimplifiedRhythm
{
OneSixthConversion = { Value = true }
},
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject>
{
new Hit { StartTime = 1000, Type = HitType.Centre },
new Hit { StartTime = 1250, Type = HitType.Centre },
new Hit { StartTime = 1500, Type = HitType.Centre },
new Hit { StartTime = 1666, Type = HitType.Rim }, // mod removes this
new Hit { StartTime = 1833, Type = HitType.Centre }, // mod moves this to 1750
new Hit { StartTime = 2000, Type = HitType.Centre },
new Hit { StartTime = 2250, Type = HitType.Centre },
},
},
ReplayFrames = new List<ReplayFrame>
{
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1200),
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
new TaikoReplayFrame(1450),
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
new TaikoReplayFrame(1600),
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
new TaikoReplayFrame(1800),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2200),
new TaikoReplayFrame(2250, TaikoAction.LeftCentre),
new TaikoReplayFrame(2450),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 6 && Player.ScoreProcessor.Accuracy.Value == 1
});
[Test]
public void TestOneEighthConversion() => CreateModTest(new ModTestData
{
Mod = new TaikoModSimplifiedRhythm
{
OneEighthConversion = { Value = true }
},
Autoplay = false,
CreateBeatmap = () =>
{
const double one_eighth_timing = 125;
return new Beatmap
{
HitObjects = new List<HitObject>
{
new Hit { StartTime = 1000, Type = HitType.Centre },
new Hit { StartTime = 1250, Type = HitType.Centre },
new Hit { StartTime = 1500, Type = HitType.Centre },
new Hit { StartTime = 1500 + one_eighth_timing * 1, Type = HitType.Rim }, // mod removes this
new Hit { StartTime = 1500 + one_eighth_timing * 2 },
new Hit { StartTime = 2000, Type = HitType.Centre },
new Hit { StartTime = 2000 + one_eighth_timing * 1, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 2000 + one_eighth_timing * 2, Type = HitType.Centre },
new Hit { StartTime = 2000 + one_eighth_timing * 3, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 2000 + one_eighth_timing * 4, Type = HitType.Centre },
new Hit { StartTime = 2000 + one_eighth_timing * 5, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 2000 + one_eighth_timing * 6, Type = HitType.Centre },
new Hit { StartTime = 2000 + one_eighth_timing * 7, Type = HitType.Centre }, // mod removes this
},
};
},
ReplayFrames = new List<ReplayFrame>
{
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1000),
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
new TaikoReplayFrame(1250),
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
new TaikoReplayFrame(1500),
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
new TaikoReplayFrame(1750),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2000),
new TaikoReplayFrame(2250, TaikoAction.LeftCentre),
new TaikoReplayFrame(2250),
new TaikoReplayFrame(2500, TaikoAction.LeftCentre),
new TaikoReplayFrame(2500),
new TaikoReplayFrame(2750, TaikoAction.LeftCentre),
new TaikoReplayFrame(2750),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 8 && Player.ScoreProcessor.Accuracy.Value == 1
});
}
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Taiko.Tests
{
public partial class TestSceneReplayRecording : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
[Resolved]
private AudioManager audioManager { get; set; } = null!;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects =
{
new Hit { StartTime = 0, },
new Hit { StartTime = 5000, },
new Hit { StartTime = 10000, },
new Hit { StartTime = 15000, }
}
};
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[Test]
public void TestRecording()
{
seekTo(0);
AddStep("press D", () => InputManager.Key(Key.D));
AddAssert("left rim press recorded to replay", () => Player.Score.Replay.Frames.OfType<TaikoReplayFrame>().Any(f => f.Actions.SequenceEqual([TaikoAction.LeftRim])));
seekTo(5000);
AddStep("press F", () => InputManager.Key(Key.F));
AddAssert("left centre press recorded to replay", () => Player.Score.Replay.Frames.OfType<TaikoReplayFrame>().Any(f => f.Actions.SequenceEqual([TaikoAction.LeftCentre])));
seekTo(10000);
AddStep("press J", () => InputManager.Key(Key.J));
AddAssert("right centre press recorded to replay", () => Player.Score.Replay.Frames.OfType<TaikoReplayFrame>().Any(f => f.Actions.SequenceEqual([TaikoAction.RightCentre])));
seekTo(10000);
AddStep("press K", () => InputManager.Key(Key.K));
AddAssert("right rim press recorded to replay", () => Player.Score.Replay.Frames.OfType<TaikoReplayFrame>().Any(f => f.Actions.SequenceEqual([TaikoAction.RightRim])));
}
private void seekTo(double time)
{
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
}
}
}

View File

@@ -0,0 +1,130 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModSimplifiedRhythm : Mod, IApplicableToBeatmap
{
public override string Name => "Simplified Rhythm";
public override string Acronym => "SR";
public override double ScoreMultiplier => 0.6;
public override LocalisableString Description => "Simplify tricky rhythms!";
public override ModType Type => ModType.DifficultyReduction;
[SettingSource("1/3 to 1/2 conversion", "Converts 1/3 patterns to 1/2 rhythm.")]
public Bindable<bool> OneThirdConversion { get; } = new BindableBool();
[SettingSource("1/6 to 1/4 conversion", "Converts 1/6 patterns to 1/4 rhythm.")]
public Bindable<bool> OneSixthConversion { get; } = new BindableBool(true);
[SettingSource("1/8 to 1/4 conversion", "Converts 1/8 patterns to 1/4 rhythm.")]
public Bindable<bool> OneEighthConversion { get; } = new BindableBool();
public void ApplyToBeatmap(IBeatmap beatmap)
{
var taikoBeatmap = (TaikoBeatmap)beatmap;
var controlPointInfo = taikoBeatmap.ControlPointInfo;
Hit[] hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast<Hit>().ToArray();
if (hits.Length == 0)
return;
var conversions = new List<(int, int)>();
if (OneEighthConversion.Value) conversions.Add((8, 4));
if (OneSixthConversion.Value) conversions.Add((6, 4));
if (OneThirdConversion.Value) conversions.Add((3, 2));
bool inPattern = false;
foreach ((int baseRhythm, int adjustedRhythm) in conversions)
{
int patternStartIndex = 0;
for (int i = 1; i < hits.Length; i++)
{
double snapValue = getSnapBetweenNotes(controlPointInfo, hits[i - 1], hits[i]);
if (inPattern)
{
// pattern continues
if (snapValue == baseRhythm) continue;
inPattern = false;
processPattern(i);
}
else
{
if (snapValue == baseRhythm)
{
patternStartIndex = i - 1;
inPattern = true;
}
}
}
// Process the last pattern if we reached the end of the beatmap and are still in a pattern.
if (inPattern)
processPattern(hits.Length);
void processPattern(int patternEndIndex)
{
// Iterate through the pattern
for (int j = patternStartIndex; j < patternEndIndex; j++)
{
int indexInPattern = j - patternStartIndex;
switch (baseRhythm)
{
// 1/8: Remove every second note
case 8:
{
if (indexInPattern % 2 == 1)
{
taikoBeatmap.HitObjects.Remove(hits[j]);
}
break;
}
// 1/6 and 1/3: Remove every second note and adjust time of every third
case 6:
case 3:
{
if (indexInPattern % 3 == 1)
taikoBeatmap.HitObjects.Remove(hits[j]);
else if (indexInPattern % 3 == 2)
hits[j].StartTime = hits[j + 1].StartTime - controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / adjustedRhythm;
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(baseRhythm));
}
}
}
}
}
private int getSnapBetweenNotes(ControlPointInfo controlPointInfo, Hit currentNote, Hit nextNote)
{
var currentTimingPoint = controlPointInfo.TimingPointAt(currentNote.StartTime);
return controlPointInfo.GetClosestBeatDivisor(currentTimingPoint.Time + (nextNote.StartTime - currentNote.StartTime));
}
}
}

View File

@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
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);

View File

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

View File

@@ -0,0 +1,76 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Skinning.Default
{
public class TaikoTrianglesSkinTransformer : SkinTransformer
{
public TaikoTrianglesSkinTransformer(ISkin skin)
: base(skin)
{
}
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
switch (lookup)
{
case GlobalSkinnableContainerLookup containerLookup:
{
// Only handle per ruleset defaults here.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
switch (containerLookup.Lookup)
{
case GlobalSkinnableContainers.MainHUDComponents:
return new DefaultSkinComponentsContainer(container =>
{
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (leaderboard != null)
{
leaderboard.Position = new Vector2(40, -100);
leaderboard.Height = 180;
leaderboard.Anchor = Anchor.BottomLeft;
leaderboard.Origin = Anchor.BottomLeft;
}
if (spectatorList != null)
{
spectatorList.HeaderFont.Value = Typeface.Venera;
spectatorList.HeaderColour.Value = new OsuColour().BlueLighter;
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.TopLeft;
spectatorList.Position = new Vector2(320, -280);
}
})
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new DrawableGameplayLeaderboard(),
new SpectatorList
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
}
},
};
}
return null;
}
}
return base.GetDrawableComponent(lookup);
}
}
}

View File

@@ -3,12 +3,15 @@
using System;
using System.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);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Expanded = { Value = true }
ForceExpand = { Value = true }
}
});
});

View File

@@ -560,7 +560,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
private 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();
}

View File

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

View File

@@ -0,0 +1,90 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Tests.Gameplay;
using osu.Game.Tests.Visual.Gameplay;
namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiplayerPositionDisplay : OsuTestScene
{
[Resolved]
private OsuConfigManager config { get; set; } = null!;
private GameplayLeaderboardScore score = null!;
private readonly Bindable<int?> position = new Bindable<int?>(8);
private TestSceneGameplayLeaderboard.TestGameplayLeaderboardProvider leaderboardProvider = null!;
private MultiplayerPositionDisplay display = null!;
private GameplayState gameplayState = null!;
private const int player_count = 32;
[Test]
public void TestAppearance()
{
AddStep("create content", () =>
{
Children = new Drawable[]
{
new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(IGameplayLeaderboardProvider), leaderboardProvider = new TestSceneGameplayLeaderboard.TestGameplayLeaderboardProvider()),
(typeof(GameplayState), gameplayState = TestGameplayState.Create(new OsuRuleset()))
],
Child = display = new MultiplayerPositionDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
};
score = leaderboardProvider.CreateLeaderboardScore(new BindableLong(), API.LocalUser.Value, true);
score.Position.BindTo(position);
for (int i = 0; i < player_count - 1; i++)
{
var r = leaderboardProvider.CreateRandomScore(new APIUser());
r.Position.Value = i;
}
});
AddSliderStep("set score position", 1, player_count, position.Value!.Value, r => position.Value = r);
AddStep("unset position", () => position.Value = null);
AddStep("toggle leaderboardProvider on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true));
AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1));
AddStep("toggle leaderboardProvider off", () => config.SetValue(OsuSetting.GameplayLeaderboard, false));
AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0));
AddStep("enter break", () => ((Bindable<LocalUserPlayingState>)gameplayState.PlayingState).Value = LocalUserPlayingState.Break);
AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1));
AddStep("exit break", () => ((Bindable<LocalUserPlayingState>)gameplayState.PlayingState).Value = LocalUserPlayingState.Playing);
AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0));
AddStep("toggle leaderboardProvider on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true));
AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1));
AddStep("change local user", () => ((DummyAPIAccess)API).LocalUser.Value = new GuestUser());
AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0));
}
}
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
namespace osu.Game.Tests.Visual.Navigation
{
public partial class TestSceneScreenFooterNavigation : OsuGameTestScene
{
private ScreenFooter screenFooter => this.ChildrenOfType<ScreenFooter>().Single();
[Test]
public void TestFooterHidesOldBackButton()
{
PushAndConfirm(() => new TestScreen(false));
AddAssert("footer hidden", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddAssert("old back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible));
PushAndConfirm(() => new TestScreen(true));
AddAssert("footer shown", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Visible));
AddAssert("old back button hidden", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Hidden));
PushAndConfirm(() => new TestScreen(false));
AddAssert("footer hidden", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddAssert("back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible));
AddStep("exit screen", () => Game.ScreenStack.Exit());
AddAssert("footer shown", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Visible));
AddAssert("old back button hidden", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("exit screen", () => Game.ScreenStack.Exit());
AddAssert("footer hidden", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddAssert("old back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible));
}
private partial class TestScreen : OsuScreen
{
public override bool ShowFooter { get; }
public TestScreen(bool footer)
{
ShowFooter = footer;
}
}
}
}

View File

@@ -611,7 +611,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("ensure score is databased", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == false));
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());

View File

@@ -75,9 +75,11 @@ namespace osu.Game.Tests.Visual.Online
[TestCase("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", LinkAction.External)]
[TestCase("(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)]

View File

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

View File

@@ -167,6 +167,57 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("local score is #16", () => this.ChildrenOfType<ScorePanelList>().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16));
}
[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]

View File

@@ -0,0 +1,180 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Carousel;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.SongSelectV2
{
[TestFixture]
public partial class BeatmapCarouselFilterSortingTest
{
[Test]
public async Task TestSorting()
{
List<BeatmapSetInfo> beatmapSets = new List<BeatmapSetInfo>();
const string zzz_lowercase = "zzzzz";
const string zzz_uppercase = "ZZZZZ";
const int diff_count = 5;
for (int i = 0; i < 20; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
if (i == 4)
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_uppercase);
if (i == 8)
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_lowercase);
if (i == 12)
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_uppercase);
if (i == 16)
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_lowercase);
beatmapSets.Add(set);
}
var results = await runSorting(SortMode.Author, beatmapSets);
Assert.That(results.Last().Metadata.Author.Username, Is.EqualTo(zzz_uppercase));
Assert.That(results.SkipLast(diff_count).Last().Metadata.Author.Username, Is.EqualTo(zzz_lowercase));
results = await runSorting(SortMode.Artist, beatmapSets);
Assert.That(results.Last().Metadata.Artist, Is.EqualTo(zzz_uppercase));
Assert.That(results.SkipLast(diff_count).Last().Metadata.Artist, Is.EqualTo(zzz_lowercase));
}
[Test]
public async Task TestSortingDateSubmitted()
{
List<BeatmapSetInfo> beatmapSets = new List<BeatmapSetInfo>();
const string zzz_string = "zzzzz";
for (int i = 0; i < 10; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo(5);
// A total of 6 sets have date submitted (4 don't)
// A total of 5 sets have artist string (3 of which also have date submitted)
if (i >= 2 && i < 8) // i = 2, 3, 4, 5, 6, 7 have submitted date
set.DateSubmitted = DateTimeOffset.Now.AddMinutes(i);
if (i < 5) // i = 0, 1, 2, 3, 4 have matching string
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string);
set.Beatmaps.ForEach(b => b.Metadata.Title = $"submitted: {set.DateSubmitted}");
beatmapSets.Add(set);
}
var results = await runSorting(SortMode.DateSubmitted, beatmapSets);
Assert.That(results.Count(), Is.EqualTo(50));
Assert.That(results.Reverse().TakeWhile(b => b.BeatmapSet!.DateSubmitted == null).Count(), Is.EqualTo(20), () => "missing dates should be at the end");
Assert.That(results.TakeWhile(b => b.BeatmapSet!.DateSubmitted != null).Count(), Is.EqualTo(30), () => "non-missing dates should be at the start");
}
[Test]
public async Task TestSortByArtistUsesTitleAsTiebreaker()
{
List<BeatmapSetInfo> beatmapSets = new List<BeatmapSetInfo>();
const int diff_count = 5;
for (int i = 0; i < 20; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
if (i == 4)
{
set.Beatmaps.ForEach(b =>
{
b.Metadata.Artist = "ZZZ";
b.Metadata.Title = "AAA";
});
}
if (i == 8)
{
set.Beatmaps.ForEach(b =>
{
b.Metadata.Artist = "ZZZ";
b.Metadata.Title = "ZZZ";
});
}
beatmapSets.Add(set);
}
var results = await runSorting(SortMode.Artist, beatmapSets);
Assert.That(() =>
{
var lastItem = results.Last();
return lastItem.Metadata.Artist == "ZZZ" && lastItem.Metadata.Title == "ZZZ";
});
Assert.That(() =>
{
var secondLastItem = results.SkipLast(diff_count).Last();
return secondLastItem.Metadata.Artist == "ZZZ" && secondLastItem.Metadata.Title == "AAA";
});
}
/// <summary>
/// Ensures stability is maintained on different sort modes for items with equal properties.
/// </summary>
[Test]
public async Task TestSortingStabilityDateAdded()
{
List<BeatmapSetInfo> beatmapSets = new List<BeatmapSetInfo>();
for (int i = 0; i < 10; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo();
set.DateAdded = DateTimeOffset.FromUnixTimeSeconds(i);
// only need to set the first as they are a shared reference.
var beatmap = set.Beatmaps.First();
beatmap.Metadata.Artist = "a";
beatmap.Metadata.Title = "b";
beatmapSets.Add(set);
}
var results = await runSorting(SortMode.Title, beatmapSets);
Assert.That(results.Select(b => b.BeatmapSet!.DateAdded), Is.Ordered.Descending);
results = await runSorting(SortMode.Artist, beatmapSets);
Assert.That(results.Select(b => b.BeatmapSet!.DateAdded), Is.Ordered.Descending);
}
private static async Task<IEnumerable<BeatmapInfo>> runSorting(SortMode sort, List<BeatmapSetInfo> beatmapSets)
{
var sorter = new BeatmapCarouselFilterSorting(() => new FilterCriteria { Sort = sort });
var carouselItems = await sorter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))), CancellationToken.None);
return carouselItems.Select(ci => ci.Model).OfType<BeatmapInfo>();
}
}
}

View File

@@ -4,9 +4,11 @@
using System;
using System.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;
}
}
}
}

View File

@@ -0,0 +1,204 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public abstract partial class SongSelectTestScene : ScreenTestScene
{
protected BeatmapManager Beatmaps { get; private set; } = null!;
protected RealmRulesetStore Rulesets { get; private set; } = null!;
protected OsuConfigManager Config { get; private set; } = null!;
protected ScoreManager ScoreManager { get; private set; } = null!;
private RealmDetachedBeatmapStore beatmapStore = null!;
protected Screens.SelectV2.SongSelect SongSelect { get; private set; } = null!;
protected BeatmapCarousel Carousel => SongSelect.ChildrenOfType<BeatmapCarousel>().Single();
[Cached]
protected readonly ScreenFooter Footer;
[Cached]
private readonly OsuLogo logo;
[Cached]
private readonly VolumeOverlay volume;
[Cached(typeof(INotificationOverlay))]
private readonly INotificationOverlay notificationOverlay = new NotificationOverlay();
[Cached]
protected readonly LeaderboardManager LeaderboardManager = new LeaderboardManager();
protected SongSelectTestScene()
{
Children = new Drawable[]
{
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
LeaderboardManager,
new Toolbar
{
State = { Value = Visibility.Visible },
},
Footer = new ScreenFooter
{
BackButtonPressed = () => Stack.CurrentScreen.Exit(),
},
logo = new OsuLogo
{
Alpha = 0f,
},
volume = new VolumeOverlay(),
},
},
};
Stack.Padding = new MarginPadding { Top = Toolbar.HEIGHT };
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
// These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install.
// At a point we have isolated interactive test runs enough, this can likely be removed.
dependencies.Cache(Rulesets = new RealmRulesetStore(Realm));
dependencies.Cache(Realm);
dependencies.Cache(Beatmaps = new BeatmapManager(LocalStorage, Realm, null, Dependencies.Get<AudioManager>(), Resources, Dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(Config = new OsuConfigManager(LocalStorage));
dependencies.Cache(ScoreManager = new ScoreManager(Rulesets, () => Beatmaps, LocalStorage, Realm, API, Config));
dependencies.CacheAs<BeatmapStore>(beatmapStore = new RealmDetachedBeatmapStore());
return dependencies;
}
[BackgroundDependencyLoader]
private void load()
{
Add(beatmapStore);
}
protected override void LoadComplete()
{
base.LoadComplete();
Stack.ScreenPushed += updateFooter;
Stack.ScreenExited += updateFooter;
}
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("reset defaults", () =>
{
Ruleset.Value = Rulesets.AvailableRulesets.First();
Beatmap.SetDefault();
SelectedMods.SetDefault();
Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title);
Config.SetValue(OsuSetting.SongSelectGroupingMode, GroupMode.All);
SongSelect = null!;
});
AddStep("delete all beatmaps", () => Beatmaps.Delete());
}
protected virtual void LoadSongSelect()
{
AddStep("load screen", () => Stack.Push(SongSelect = new SoloSongSelect()));
AddUntilStep("wait for load", () => Stack.CurrentScreen == SongSelect && SongSelect.IsLoaded);
AddUntilStep("wait for filtering", () => !Carousel.IsFiltering);
}
protected void ImportBeatmapForRuleset(int rulesetId)
{
int beatmapsCount = 0;
AddStep($"import test map for ruleset {rulesetId}", () =>
{
beatmapsCount = SongSelect.IsNull() ? 0 : Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single().SetItems.Count;
Beatmaps.Import(TestResources.CreateTestBeatmapSetInfo(3, Rulesets.AvailableRulesets.Where(r => r.OnlineID == rulesetId).ToArray()));
});
// This is specifically for cases where the add is happening post song select load.
// For cases where song select is null, the assertions are provided by the load checks.
AddUntilStep("wait for imported to arrive in carousel", () => SongSelect.IsNull() || Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single().SetItems.Count > beatmapsCount);
}
protected void ChangeMods(params Mod[] mods) => AddStep($"change mods to {string.Join(", ", mods.Select(m => m.Acronym))}", () => SelectedMods.Value = mods);
protected void ChangeRuleset(int rulesetId)
{
AddStep($"change ruleset to {rulesetId}", () => Ruleset.Value = Rulesets.AvailableRulesets.First(r => r.OnlineID == rulesetId));
}
/// <summary>
/// Imports test beatmap sets to show in the carousel.
/// </summary>
/// <param name="difficultyCountPerSet">
/// The exact count of difficulties to create for each beatmap set.
/// A <see langword="null"/> value causes the count of difficulties to be selected randomly.
/// </param>
protected void AddManyTestMaps(int? difficultyCountPerSet = null)
{
AddStep("import test maps", () =>
{
var usableRulesets = Rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray();
for (int i = 0; i < 10; i++)
Beatmaps.Import(TestResources.CreateTestBeatmapSetInfo(difficultyCountPerSet, usableRulesets));
});
}
protected void WaitForSuspension() => AddUntilStep("wait for not current", () => !SongSelect.AsNonNull().IsCurrentScreen());
private void updateFooter(IScreen? _, IScreen? newScreen)
{
if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter)
{
Footer.Show();
Footer.SetButtons(osuScreen.CreateFooterButtons());
}
else
{
Footer.Hide();
Footer.SetButtons(Array.Empty<ScreenFooterButton>());
}
}
}
}

View File

@@ -6,7 +6,6 @@ using System.Threading.Tasks;
using NUnit.Framework;
using 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]

View File

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

View File

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

View File

@@ -0,0 +1,294 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.SongSelectV2
{
[TestFixture]
public partial class TestSceneBeatmapCarouselFiltering : BeatmapCarouselTestScene
{
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
}
[Test]
public void TestBasicFiltering()
{
AddBeatmaps(10, 3);
WaitForDrawablePanels();
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(1));
ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
WaitForFiltering();
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(2));
CheckDisplayedBeatmapSetsCount(1);
CheckDisplayedBeatmapsCount(3);
SelectNextPanel();
Select();
WaitForSelection(2, 0);
for (int i = 0; i < 5; i++)
SelectNextPanel();
Select();
WaitForSelection(2, 1);
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
WaitForFiltering();
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(3));
CheckDisplayedBeatmapSetsCount(10);
CheckDisplayedBeatmapsCount(30);
}
[Test]
public void TestFilteringByUserStarDifficulty()
{
AddStep("add mixed difficulty set", () =>
{
var set = TestResources.CreateTestBeatmapSetInfo(1);
set.Beatmaps.Clear();
for (int i = 1; i <= 15; i++)
{
set.Beatmaps.Add(new BeatmapInfo(new OsuRuleset().RulesetInfo, new BeatmapDifficulty(), new BeatmapMetadata())
{
BeatmapSet = set,
DifficultyName = $"Stars: {i}",
StarRating = i,
});
}
BeatmapSets.Add(set);
});
WaitForDrawablePanels();
ApplyToFilter("filter [5..]", c =>
{
c.UserStarDifficulty.Min = 5;
c.UserStarDifficulty.Max = null;
});
WaitForFiltering();
CheckDisplayedBeatmapsCount(11);
ApplyToFilter("filter to [0..7]", c =>
{
c.UserStarDifficulty.Min = null;
c.UserStarDifficulty.Max = 7;
});
WaitForFiltering();
CheckDisplayedBeatmapsCount(7);
ApplyToFilter("filter to [5..7]", c =>
{
c.UserStarDifficulty.Min = 5;
c.UserStarDifficulty.Max = 7;
});
WaitForFiltering();
CheckDisplayedBeatmapsCount(3);
ApplyToFilter("filter to [2..2]", c =>
{
c.UserStarDifficulty.Min = 2;
c.UserStarDifficulty.Max = 2;
});
WaitForFiltering();
CheckDisplayedBeatmapsCount(1);
ApplyToFilter("filter to [0..]", c =>
{
c.UserStarDifficulty.Min = 0;
c.UserStarDifficulty.Max = null;
});
WaitForFiltering();
CheckDisplayedBeatmapsCount(15);
}
[Test]
public void TestCarouselRemembersSelection()
{
Guid selectedID = Guid.Empty;
AddBeatmaps(50, 3);
WaitForDrawablePanels();
SelectNextGroup();
SelectNextPanel();
Select();
AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID);
for (int i = 0; i < 5; i++)
{
ApplyToFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString());
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
}
}
[Test]
public void TestCarouselRemembersSelectionDifficultySort()
{
Guid selectedID = Guid.Empty;
AddBeatmaps(50, 3);
WaitForDrawablePanels();
SortBy(SortMode.Difficulty);
SelectNextGroup();
AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID);
for (int i = 0; i < 5; i++)
{
ApplyToFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString());
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
ApplyToFilter("remove filter", c => c.SearchText = string.Empty);
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
}
}
[Test]
public void TestCarouselRetainsSelectionFromDifficultySort()
{
AddBeatmaps(50, 3);
WaitForDrawablePanels();
BeatmapInfo chosenBeatmap = null!;
for (int i = 0; i < 3; i++)
{
int diff = i;
AddStep($"select diff {diff}", () => Carousel.CurrentSelection = chosenBeatmap = BeatmapSets[20].Beatmaps[diff]);
AddUntilStep("selection changed", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
SortBy(SortMode.Difficulty);
AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
SortBy(SortMode.Title);
AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
}
}
[Test]
public void TestExternalRulesetChange()
{
ApplyToFilter("allow converted beatmaps", c => c.AllowConvertedBeatmaps = true);
ApplyToFilter("filter to osu", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(0));
WaitForFiltering();
AddStep("add mixed ruleset beatmapset", () =>
{
var testMixed = TestResources.CreateTestBeatmapSetInfo(3);
for (int i = 0; i <= 2; i++)
testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i);
BeatmapSets.Add(testMixed);
});
WaitForDrawablePanels();
SelectNextPanel();
Select();
AddUntilStep("wait for filtered difficulties", () =>
{
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
return visibleBeatmapPanels.Count() == 1
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1;
});
ApplyToFilter("filter to taiko", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(1));
WaitForFiltering();
AddUntilStep("wait for filtered difficulties", () =>
{
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
return visibleBeatmapPanels.Count() == 2
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 1) == 1;
});
ApplyToFilter("filter to catch", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(2));
WaitForFiltering();
AddUntilStep("wait for filtered difficulties", () =>
{
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
return visibleBeatmapPanels.Count() == 2
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 2) == 1;
});
}
[Test]
public void TestSortingWithDifficultyFiltered()
{
const int diffs_per_set = 3;
const int local_set_count = 2;
AddStep("populate beatmap sets", () =>
{
for (int i = 0; i < local_set_count; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo(diffs_per_set);
set.Beatmaps[0].StarRating = 3 - i;
set.Beatmaps[0].DifficultyName += $" ({3 - i}*)";
set.Beatmaps[1].StarRating = 6 + i;
set.Beatmaps[1].DifficultyName += $" ({6 + i}*)";
BeatmapSets.Add(set);
}
});
SortBy(SortMode.Difficulty);
WaitForFiltering();
CheckDisplayedBeatmapsCount(local_set_count * diffs_per_set);
ApplyToFilter("filter to normal", c => c.SearchText = "Normal");
WaitForFiltering();
CheckDisplayedBeatmapsCount(local_set_count);
ApplyToFilter("filter to insane", c => c.SearchText = "Insane");
WaitForFiltering();
CheckDisplayedBeatmapsCount(local_set_count);
}
}
}

View File

@@ -6,7 +6,6 @@ using NUnit.Framework;
using osu.Framework.Testing;
using osu.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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,253 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Chat;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Select.Filter;
using FilterControl = osu.Game.Screens.SelectV2.FilterControl;
using NoResultsPlaceholder = osu.Game.Screens.SelectV2.NoResultsPlaceholder;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneSongSelectFiltering : SongSelectTestScene
{
private FilterControl filter => SongSelect.ChildrenOfType<FilterControl>().Single();
private ShearedFilterTextBox filterTextBox => SongSelect.ChildrenOfType<ShearedFilterTextBox>().Single();
private int filterOperationsCount;
protected override void LoadSongSelect()
{
base.LoadSongSelect();
AddStep("hook filter event", () =>
{
filterOperationsCount = 0;
filter.CriteriaChanged += _ => filterOperationsCount++;
});
}
[Test]
public void TestSingleFilterOnEnter()
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
}
[Test]
public void TestNoFilterOnSimpleResume()
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
WaitForSuspension();
AddStep("return", () => SongSelect.MakeCurrent());
AddUntilStep("wait for current", () => SongSelect.IsCurrentScreen());
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
}
[Test]
public void TestFilterOnResumeAfterChange()
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
AddStep("change convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
LoadSongSelect();
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
WaitForSuspension();
AddStep("change convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
AddStep("return", () => SongSelect.MakeCurrent());
AddUntilStep("wait for current", () => SongSelect.IsCurrentScreen());
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
}
[Test]
public void TestSorting()
{
LoadSongSelect();
AddManyTestMaps();
// TODO: old test has this step, but there doesn't seem to be any purpose for it.
// AddUntilStep("random map selected", () => Beatmap.Value != defaultBeatmap);
AddStep(@"Sort by Artist", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
AddStep(@"Sort by Title", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
AddStep(@"Sort by Author", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Author));
AddStep(@"Sort by DateAdded", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.DateAdded));
AddStep(@"Sort by BPM", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.BPM));
AddStep(@"Sort by Length", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Length));
AddStep(@"Sort by Difficulty", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Difficulty));
AddStep(@"Sort by Source", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Source));
}
[Test]
public void TestCutInFilterTextBox()
{
LoadSongSelect();
AddStep("set filter text", () => filterTextBox.Current.Value = "nonono");
AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll));
AddStep("press ctrl/cmd-x", () => InputManager.Keys(PlatformAction.Cut));
AddAssert("filter text cleared", () => filterTextBox.Current.Value, () => Is.Empty);
}
[Test]
public void TestNonFilterableModChange()
{
ImportBeatmapForRuleset(0);
LoadSongSelect();
// Mod that is guaranteed to never re-filter.
AddStep("add non-filterable mod", () => SelectedMods.Value = new Mod[] { new OsuModCinema() });
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
// Removing the mod should still not re-filter.
AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty<Mod>());
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
}
[Test]
public void TestFilterableModChange()
{
ImportBeatmapForRuleset(3);
LoadSongSelect();
// Change to mania ruleset.
AddStep("filter to mania ruleset", () => Ruleset.Value = Rulesets.AvailableRulesets.First(r => r.OnlineID == 3));
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
// Apply a mod, but this should NOT re-filter because there's no search text.
AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() });
AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1));
// Set search text. Should re-filter.
AddStep("set search text to match mods", () => filterTextBox.Current.Value = "keys=3");
AddAssert("filter count is 2", () => filterOperationsCount, () => Is.EqualTo(2));
// Change filterable mod. Should re-filter.
AddStep("change new filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey5() });
AddAssert("filter count is 3", () => filterOperationsCount, () => Is.EqualTo(3));
// Add non-filterable mod. Should NOT re-filter.
AddStep("apply non-filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail(), new ManiaModKey5() });
AddAssert("filter count is 3", () => filterOperationsCount, () => Is.EqualTo(3));
// Remove filterable mod. Should re-filter.
AddStep("remove filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail() });
AddAssert("filter count is 4", () => filterOperationsCount, () => Is.EqualTo(4));
// Remove non-filterable mod. Should NOT re-filter.
AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty<Mod>());
AddAssert("filter count is 4", () => filterOperationsCount, () => Is.EqualTo(4));
// Add filterable mod. Should re-filter.
AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() });
AddAssert("filter count is 5", () => filterOperationsCount, () => Is.EqualTo(5));
}
[Test]
public void TestPlaceholderVisibleAfterDeleteAll()
{
LoadSongSelect();
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
ImportBeatmapForRuleset(0);
AddUntilStep("wait for placeholder hidden", () => getPlaceholder()?.State.Value == Visibility.Hidden);
AddStep("delete all beatmaps", () => Beatmaps.Delete());
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
}
[Test]
public void TestPlaceholderVisibleAfterStarDifficultyFilter()
{
ImportBeatmapForRuleset(0);
AddStep("change star filter", () => Config.SetValue(OsuSetting.DisplayStarsMinimum, 10.0));
LoadSongSelect();
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType<DrawableLinkCompiler>().First().TriggerClick());
AddUntilStep("star filter reset", () => Config.Get<double>(OsuSetting.DisplayStarsMinimum) == 0.0);
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden);
}
[Test]
public void TestPlaceholderVisibleWithConvertSetting()
{
ImportBeatmapForRuleset(0);
AddStep("change convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
LoadSongSelect();
ChangeRuleset(2);
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType<DrawableLinkCompiler>().First().TriggerClick());
AddUntilStep("convert setting changed", () => Config.Get<bool>(OsuSetting.ShowConvertedBeatmaps));
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden);
}
[Test]
public void TestCorrectMatchCountAfterDeleteAll()
{
LoadSongSelect();
checkMatchedBeatmaps(0);
ImportBeatmapForRuleset(0);
checkMatchedBeatmaps(3);
AddStep("delete all beatmaps", () => Beatmaps.Delete());
checkMatchedBeatmaps(0);
}
[Test]
public void TestCorrectMatchCountAfterHardDelete()
{
LoadSongSelect();
checkMatchedBeatmaps(0);
ImportBeatmapForRuleset(0);
checkMatchedBeatmaps(3);
AddStep("hard delete beatmap", () => Realm.Write(r => r.RemoveRange(r.All<BeatmapSetInfo>().Where(s => !s.Protected))));
checkMatchedBeatmaps(0);
}
private NoResultsPlaceholder? getPlaceholder() => SongSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault();
private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));
}
}

View File

@@ -1,32 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Screens.Menu;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneSongSelectNavigation : OsuGameTestScene
{
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddWaitStep("wait", 5);
PushAndConfirm(() => new Screens.SelectV2.SoloSongSelect());
}
[Test]
public void TestClickLogo()
{
AddStep("click", () =>
{
InputManager.MoveMouseTo(Game.ChildrenOfType<OsuLogo>().Single());
InputManager.Click(MouseButton.Left);
});
}
}
}

View File

@@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Replays;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Tests.Visual
{
public partial class TestSceneReplayStability : ReplayStabilityTestScene
{
[Test]
public void TestOutrageouslyLargeLeadInTime()
{
// "graciously borrowed" from https://osu.ppy.sh/beatmapsets/948643#osu/1981090
const double lead_in_time = 2147272727;
const double hit_circle_time = 100;
var beatmap = new OsuBeatmap
{
HitObjects =
{
new HitCircle
{
StartTime = hit_circle_time,
Position = OsuPlayfield.BASE_SIZE / 2
}
},
AudioLeadIn = lead_in_time,
BeatmapInfo =
{
Ruleset = new OsuRuleset().RulesetInfo,
},
};
var replay = new Replay
{
Frames = Enumerable.Range(0, 300).Select(t => new OsuReplayFrame(-lead_in_time + 40 * t, new Vector2(t), t % 2 == 0 ? [] : [OsuAction.LeftButton]))
.Concat([
new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2),
new OsuReplayFrame(hit_circle_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
new OsuReplayFrame(hit_circle_time + 20, OsuPlayfield.BASE_SIZE / 2),
])
.Cast<ReplayFrame>()
.ToList(),
};
RunTest(beatmap, replay, [HitResult.Great]);
}
}
}

View File

@@ -13,9 +13,10 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneBackButton : OsuTestScene
{
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());
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,8 +40,6 @@ namespace osu.Game.Rulesets.UI
this.target = target;
RelativeSizeAxes = Axes.Both;
Depth = float.MinValue;
}
protected override void LoadComplete()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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