1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 18:47:27 +08:00

Merge branch 'master' into update-framework

This commit is contained in:
Dean Herbert 2022-11-24 01:27:58 +09:00
commit b381b4fd2d
51 changed files with 1145 additions and 245 deletions

View File

@ -35,6 +35,8 @@ namespace osu.Game.Rulesets.Catch.UI
private void load(CatchInputManager catchInputManager, OsuColour colours)
{
const float width = 0.15f;
// Ratio between normal move area height and total input height
const float normal_area_height_ratio = 0.45f;
keyBindingContainer = catchInputManager.KeyBindingContainer;
@ -54,18 +56,18 @@ namespace osu.Game.Rulesets.Catch.UI
Width = width,
Children = new Drawable[]
{
leftDashBox = new InputArea(TouchCatchAction.DashLeft, trackedActionSources)
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
},
leftBox = new InputArea(TouchCatchAction.MoveLeft, trackedActionSources)
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Height = normal_area_height_ratio,
Colour = colours.Gray9,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
leftDashBox = new InputArea(TouchCatchAction.DashLeft, trackedActionSources)
{
RelativeSizeAxes = Axes.Both,
Height = 1 - normal_area_height_ratio,
},
}
},
@ -80,15 +82,15 @@ namespace osu.Game.Rulesets.Catch.UI
rightBox = new InputArea(TouchCatchAction.MoveRight, trackedActionSources)
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Height = normal_area_height_ratio,
Colour = colours.Gray9,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
rightDashBox = new InputArea(TouchCatchAction.DashRight, trackedActionSources)
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Height = 1 - normal_area_height_ratio,
},
}
},

View File

@ -23,6 +23,15 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
/// <summary>
/// Diagrams in this class are represented as:
/// - : time
/// O : note
/// [ ] : hold note
///
/// x : button press
/// o : button release
/// </summary>
public class TestSceneHoldNoteInput : RateAdjustedBeatmapTestScene
{
private const double time_before_head = 250;
@ -223,6 +232,149 @@ namespace osu.Game.Rulesets.Mania.Tests
assertTailJudgement(HitResult.Meh);
}
/// <summary>
/// -----[ ]-O-------------
/// xo o
/// </summary>
[Test]
public void TestPressAndReleaseJustBeforeTailWithNearbyNoteAndCloseByHead()
{
Note note;
const int duration = 50;
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
// hold note is very short, to make the head still in range
new HoldNote
{
StartTime = time_head,
Duration = duration,
Column = 0,
},
{
// Next note within tail lenience
note = new Note
{
StartTime = time_head + duration + 10
}
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head + duration, ManiaAction.Key1),
new ManiaReplayFrame(time_head + duration + 10),
}, beatmap);
assertHeadJudgement(HitResult.Good);
assertTailJudgement(HitResult.Perfect);
assertHitObjectJudgement(note, HitResult.Miss);
}
/// <summary>
/// -----[ ]--O--
/// xo o
/// </summary>
[Test]
public void TestPressAndReleaseJustBeforeTailWithNearbyNote()
{
Note note;
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = time_head,
Duration = time_tail - time_head,
Column = 0,
},
{
// Next note within tail lenience
note = new Note
{
StartTime = time_tail + 50
}
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_tail - 10, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
}, beatmap);
assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
assertHitObjectJudgement(note, HitResult.Good);
}
/// <summary>
/// -----[ ]--O--
/// xo o
/// </summary>
[Test]
public void TestPressAndReleaseJustAfterTailWithNearbyNote()
{
Note note;
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = time_head,
Duration = time_tail - time_head,
Column = 0,
},
{
// Next note within tail lenience
note = new Note
{
StartTime = time_tail + 50
}
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_tail + 10, ManiaAction.Key1),
new ManiaReplayFrame(time_tail + 20),
}, beatmap);
assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
assertHitObjectJudgement(note, HitResult.Great);
}
/// <summary>
/// -----[ ]-----
/// xo o
@ -351,20 +503,23 @@ namespace osu.Game.Rulesets.Mania.Tests
.All(j => j.Type.IsHit()));
}
private void assertHitObjectJudgement(HitObject hitObject, HitResult result)
=> AddAssert($"object judged as {result}", () => judgementResults.First(j => j.HitObject == hitObject).Type, () => Is.EqualTo(result));
private void assertHeadJudgement(HitResult result)
=> AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type == result);
=> AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type, () => Is.EqualTo(result));
private void assertTailJudgement(HitResult result)
=> AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type == result);
=> AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type, () => Is.EqualTo(result));
private void assertNoteJudgement(HitResult result)
=> AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type == result);
=> AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result));
private void assertTickJudgement(HitResult result)
=> AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Any(j => j.Type == result));
=> AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Select(j => j.Type), () => Does.Contain(result));
private void assertLastTickJudgement(HitResult result)
=> AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type == result);
=> AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type, () => Is.EqualTo(result));
private ScoreAccessibleReplayPlayer currentPlayer;

View File

@ -262,14 +262,24 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
tick.MissForcefully();
}
ApplyResult(r => r.Type = Tail.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
endHold();
if (Tail.IsHit)
ApplyResult(r => r.Type = r.Judgement.MaxResult);
else
MissForcefully();
}
if (Tail.Judged && !Tail.IsHit)
HoldBrokenTime = Time.Current;
}
public override void MissForcefully()
{
base.MissForcefully();
// Important that this is always called when a result is applied.
endHold();
}
public bool OnPressed(KeyBindingPressEvent<ManiaAction> e)
{
if (AllJudged)

View File

@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// <summary>
/// Causes this <see cref="DrawableManiaHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary>
public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
public virtual void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
}
public abstract class DrawableManiaHitObject<TObject> : DrawableManiaHitObject

View File

@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
},
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Height = 0.82f,
Masking = true,
@ -54,6 +56,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
RelativeSizeAxes = Axes.X,
Height = ArgonNotePiece.CORNER_RADIUS * 2,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
},
};
}

View File

@ -52,11 +52,13 @@ namespace osu.Game.Rulesets.Osu.Skinning
protected Texture? Texture { get; set; }
private float radius => Texture?.DisplayWidth * 0.165f ?? 3;
private float height => Texture?.DisplayHeight * 0.165f ?? 3;
private float width => Texture?.DisplayWidth * 0.165f ?? 3;
protected readonly List<SmokePoint> SmokePoints = new List<SmokePoint>();
private float pointInterval => radius * 7f / 8;
private float pointInterval => width * 7f / 8;
private double smokeStartTime { get; set; } = double.MinValue;
@ -179,7 +181,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
private readonly List<SmokePoint> points = new List<SmokePoint>();
private IVertexBatch<TexturedVertex2D>? quadBatch;
private float radius;
private float width;
private float height;
private Vector2 drawSize;
private Texture? texture;
private int rotationSeed;
@ -202,7 +205,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
base.ApplyState();
radius = Source.radius;
width = Source.width;
height = Source.height;
drawSize = Source.DrawSize;
texture = Source.Texture;
@ -334,11 +338,13 @@ namespace osu.Game.Rulesets.Osu.Skinning
var dir = PointDirection(point, index);
var ortho = dir.PerpendicularLeft;
dir *= scale * width;
ortho *= scale * height;
var localTopLeft = point.Position + (radius * scale * (-ortho - dir));
var localTopRight = point.Position + (radius * scale * (-ortho + dir));
var localBotLeft = point.Position + (radius * scale * (ortho - dir));
var localBotRight = point.Position + (radius * scale * (ortho + dir));
var localTopLeft = point.Position - ortho - dir;
var localTopRight = point.Position - ortho + dir;
var localBotLeft = point.Position + ortho - dir;
var localBotRight = point.Position + ortho + dir;
quadBatch.Add(new TexturedVertex2D
{

View File

@ -26,8 +26,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
};
[Test]
public void DrumrollTest()
public void TestDrumroll([Values] bool withKiai)
{
AddStep("set up beatmap", () => setUpBeatmap(withKiai));
AddStep("Drum roll", () => SetContents(_ =>
{
var hoc = new ScrollingHitObjectContainer();
@ -73,5 +75,22 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
return drumroll;
}
private void setUpBeatmap(bool withKiai)
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
if (withKiai)
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
ControlPointInfo = controlPointInfo
});
Beatmap.Value.Track.Start();
}
}
}

View File

@ -1,30 +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 NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneDrawableDrumRollKiai : TestSceneDrawableDrumRoll
{
[SetUp]
public void SetUp() => Schedule(() =>
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
ControlPointInfo = controlPointInfo
});
// track needs to be playing for BeatSyncedContainer to work.
Beatmap.Value.Track.Start();
});
}
}

View File

@ -4,19 +4,51 @@
#nullable disable
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Screens.Play;
using osu.Game.Tests.Gameplay;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneDrawableHit : TaikoSkinnableTestScene
{
[Cached]
private GameplayState gameplayState = TestGameplayState.Create(new TaikoRuleset());
[Test]
public void TestHits()
public void TestHits([Values] bool withKiai)
{
AddStep("Create beatmap", () => setUpBeatmap(withKiai));
addHitSteps();
}
[Test]
public void TestHitAnimationSlow()
{
AddStep("Create beatmap", () => setUpBeatmap(false));
AddStep("Set 50 combo", () => gameplayState.ScoreProcessor.Combo.Value = 50);
addHitSteps();
AddStep("Reset combo", () => gameplayState.ScoreProcessor.Combo.Value = 0);
}
[Test]
public void TestHitAnimationFast()
{
AddStep("Create beatmap", () => setUpBeatmap(false));
AddStep("Set 150 combo", () => gameplayState.ScoreProcessor.Combo.Value = 150);
addHitSteps();
AddStep("Reset combo", () => gameplayState.ScoreProcessor.Combo.Value = 0);
}
private void addHitSteps()
{
AddStep("Centre hit", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime())
{
@ -56,5 +88,22 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
return hit;
}
private void setUpBeatmap(bool withKiai)
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
if (withKiai)
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
ControlPointInfo = controlPointInfo
});
Beatmap.Value.Track.Start();
}
}
}

View File

@ -1,30 +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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneDrawableHitKiai : TestSceneDrawableHit
{
[SetUp]
public void SetUp() => Schedule(() =>
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
ControlPointInfo = controlPointInfo
});
// track needs to be playing for BeatSyncedContainer to work.
Beatmap.Value.Track.Start();
});
}
}

View File

@ -6,10 +6,10 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{

View File

@ -2,13 +2,16 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@ -18,6 +21,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public class LegacyCirclePiece : CompositeDrawable, IHasAccentColour
{
private Drawable backgroundLayer = null!;
private Drawable? foregroundLayer;
private Bindable<int> currentCombo { get; } = new BindableInt();
private int animationFrame;
private double beatLength;
// required for editor blueprints (not sure why these circle pieces are zero size).
public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad;
@ -27,6 +36,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
RelativeSizeAxes = Axes.Both;
}
[Resolved(canBeNull: true)]
private GameplayState? gameplayState { get; set; }
[Resolved(canBeNull: true)]
private IBeatSyncProvider? beatSyncProvider { get; set; }
[BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableHitObject)
{
@ -45,7 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle")));
var foregroundLayer = getDrawableFor("circleoverlay");
foregroundLayer = getDrawableFor("circleoverlay");
if (foregroundLayer != null)
AddInternal(foregroundLayer);
@ -58,6 +73,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
c.Anchor = Anchor.Centre;
c.Origin = Anchor.Centre;
}
if (gameplayState != null)
currentCombo.BindTo(gameplayState.ScoreProcessor.Combo);
}
protected override void LoadComplete()
@ -74,6 +92,37 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
// This ensures they are scaled relative to each other but also match the expected DrawableHit size.
foreach (var c in InternalChildren)
c.Scale = new Vector2(DrawHeight / 128);
if (foregroundLayer is IFramedAnimation animatableForegroundLayer)
animateForegroundLayer(animatableForegroundLayer);
}
private void animateForegroundLayer(IFramedAnimation animatableForegroundLayer)
{
int multiplier;
if (currentCombo.Value >= 150)
{
multiplier = 2;
}
else if (currentCombo.Value >= 50)
{
multiplier = 1;
}
else
{
animatableForegroundLayer.GotoFrame(0);
return;
}
if (beatSyncProvider?.ControlPoints != null)
{
beatLength = beatSyncProvider.ControlPoints.TimingPointAt(Time.Current).BeatLength;
animationFrame = Time.Current % ((beatLength * 2) / multiplier) >= beatLength / multiplier ? 0 : 1;
animatableForegroundLayer.GotoFrame(animationFrame);
}
}
private Color4 accentColour;

View File

@ -65,7 +65,7 @@ namespace osu.Game.Tests.Database
private class TestLegacyBeatmapImporter : LegacyBeatmapImporter
{
public TestLegacyBeatmapImporter()
: base(null)
: base(null!)
{
}

View File

@ -5,7 +5,7 @@
using System;
using NUnit.Framework;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Utils;
namespace osu.Game.Tests.NonVisual
{

View File

@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
public void TestSharedClockState()
public void TestClockTimeTransferIsOneDirectional()
{
AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
AddStep("click test gameplay button", () =>
@ -195,15 +195,15 @@ namespace osu.Game.Tests.Visual.Editing
GameplayClockContainer gameplayClockContainer = null;
AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType<GameplayClockContainer>().First());
AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
// when the gameplay test is entered, the clock is expected to continue from where it was in the main editor...
AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);
double timeAtPlayerExit = 0;
AddWaitStep("wait some", 5);
AddStep("store time before exit", () => timeAtPlayerExit = gameplayClockContainer.CurrentTime);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddAssert("time is past player exit", () => EditorClock.CurrentTime >= timeAtPlayerExit);
// but when exiting from gameplay test back to editor, the expectation is that the editor time should revert to what it was at the point of initiating the gameplay test.
AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000));
}
public override void TearDownSteps()

View File

@ -3,14 +3,15 @@
#nullable disable
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Screens.Edit.Timing.RowAttributes;
@ -21,10 +22,6 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture]
public class TestSceneTimingScreen : EditorClockTestScene
{
[Cached(typeof(EditorBeatmap))]
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
@ -32,21 +29,27 @@ namespace osu.Game.Tests.Visual.Editing
protected override bool ScrollUsingMouseWheel => false;
public TestSceneTimingScreen()
{
editorBeatmap = new EditorBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
}
protected override void LoadComplete()
{
base.LoadComplete();
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
Beatmap.Disabled = true;
Child = timingScreen = new TimingScreen
var editorBeatmap = new EditorBeatmap(Beatmap.Value.GetPlayableBeatmap(Ruleset.Value));
Child = new DependencyProvidingContainer
{
State = { Value = Visibility.Visible },
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(EditorBeatmap), editorBeatmap),
(typeof(IBeatSnapProvider), editorBeatmap)
},
Child = timingScreen = new TimingScreen
{
State = { Value = Visibility.Visible },
},
};
}

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select;
namespace osu.Game.Tests.Visual.Gameplay
{
@ -26,6 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private readonly BindableList<ScoreInfo> scores = new BindableList<ScoreInfo>();
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
private readonly Bindable<PlayBeatmapDetailArea.TabType> beatmapTabType = new Bindable<PlayBeatmapDetailArea.TabType>();
private SoloGameplayLeaderboard leaderboard = null!;
@ -33,6 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
config.BindWith(OsuSetting.BeatmapDetailTab, beatmapTabType);
}
[SetUpSteps]
@ -70,6 +73,25 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
}
[TestCase(PlayBeatmapDetailArea.TabType.Local, 51)]
[TestCase(PlayBeatmapDetailArea.TabType.Global, null)]
[TestCase(PlayBeatmapDetailArea.TabType.Country, null)]
[TestCase(PlayBeatmapDetailArea.TabType.Friends, null)]
public void TestTrackedScorePosition(PlayBeatmapDetailArea.TabType tabType, int? expectedOverflowIndex)
{
AddStep($"change TabType to {tabType}", () => beatmapTabType.Value = tabType);
AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50));
AddStep("add one more score", () => scores.Add(new ScoreInfo { User = new APIUser { Username = "New player 1" }, TotalScore = RNG.Next(600000, 1000000) }));
AddUntilStep("wait for sort", () => leaderboard.ChildrenOfType<GameplayLeaderboardScore>().First().ScorePosition != null);
if (expectedOverflowIndex == null)
AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null);
else
AddUntilStep($"tracked player is #{expectedOverflowIndex}", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(expectedOverflowIndex));
}
[Test]
public void TestVisibility()
{
@ -95,7 +117,7 @@ namespace osu.Game.Tests.Visual.Gameplay
new ScoreInfo { User = new APIUser { Username = @"spaceman_atlas" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"frenzibyte" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"Susko3" }, TotalScore = RNG.Next(500000, 1000000) },
}.Concat(Enumerable.Range(0, 50).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList();
}.Concat(Enumerable.Range(0, 44).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList();
}
}
}

View File

@ -4,6 +4,7 @@
#nullable disable
using System.Linq;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
@ -85,6 +86,19 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("did perform", () => actionPerformed);
}
[Test]
public void TestPerformEnsuresScreenIsLoaded()
{
TestLoadBlockingScreen screen = null;
AddStep("push blocking screen", () => Game.ScreenStack.Push(screen = new TestLoadBlockingScreen()));
AddStep("perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestLoadBlockingScreen) }));
AddAssert("action not performed", () => !actionPerformed);
AddStep("allow load", () => screen.LoadEvent.Set());
AddUntilStep("action performed", () => actionPerformed);
}
[Test]
public void TestOverlaysAlwaysClosed()
{
@ -270,5 +284,16 @@ namespace osu.Game.Tests.Visual.Navigation
return base.OnExiting(e);
}
}
public class TestLoadBlockingScreen : OsuScreen
{
public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim();
[BackgroundDependencyLoader]
private void load()
{
LoadEvent.Wait(10000);
}
}
}
}

View File

@ -7,12 +7,14 @@ using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
namespace osu.Game.Tests.Visual.Navigation
{
@ -55,6 +57,7 @@ namespace osu.Game.Tests.Visual.Navigation
presentAndConfirm(firstImport);
var secondImport = importBeatmap(3);
confirmBeatmapInSongSelect(secondImport);
presentAndConfirm(secondImport);
// Test presenting same beatmap more than once
@ -74,6 +77,7 @@ namespace osu.Game.Tests.Visual.Navigation
presentAndConfirm(firstImport);
var secondImport = importBeatmap(3, new ManiaRuleset().RulesetInfo);
confirmBeatmapInSongSelect(secondImport);
presentAndConfirm(secondImport);
presentSecondDifficultyAndConfirm(firstImport, 1);
@ -134,13 +138,22 @@ namespace osu.Game.Tests.Visual.Navigation
return () => imported;
}
private void confirmBeatmapInSongSelect(Func<BeatmapSetInfo> getImport)
{
AddUntilStep("beatmap in song select", () =>
{
var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen;
return songSelect.ChildrenOfType<BeatmapCarousel>().Single().BeatmapSets.Any(b => b.MatchesOnlineID(getImport()));
});
}
private void presentAndConfirm(Func<BeatmapSetInfo> getImport)
{
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect);
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.MatchesOnlineID(getImport()));
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.Equals(getImport().Beatmaps.First().Ruleset));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.IsLoaded);
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID, () => Is.EqualTo(getImport().OnlineID));
AddAssert("correct ruleset selected", () => Game.Ruleset.Value, () => Is.EqualTo(getImport().Beatmaps.First().Ruleset));
}
private void presentSecondDifficultyAndConfirm(Func<BeatmapSetInfo> getImport, int importedID)
@ -148,9 +161,9 @@ namespace osu.Game.Tests.Visual.Navigation
Predicate<BeatmapInfo> pred = b => b.OnlineID == importedID * 2048;
AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect);
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID == importedID * 2048);
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.Equals(getImport().Beatmaps.First().Ruleset));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.IsLoaded);
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(importedID * 2048));
AddAssert("correct ruleset selected", () => Game.Ruleset.Value, () => Is.EqualTo(getImport().Beatmaps.First().Ruleset));
}
}
}

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
@ -32,6 +33,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Select(i => (float)i));
AddStep("values from 1-100", () => graph.Values = Enumerable.Range(1, 100).Select(i => (float)i));
AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().Select(i => (float)i));
AddStep("empty values", () => graph.Values = Array.Empty<float>());
AddStep("Bottom to top", () => graph.Direction = BarDirection.BottomToTop);
AddStep("Top to bottom", () => graph.Direction = BarDirection.TopToBottom);
AddStep("Left to right", () => graph.Direction = BarDirection.LeftToRight);

View File

@ -1,14 +1,15 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile.Header.Components;
@ -23,33 +24,14 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
public TestSceneRankGraph()
private RankGraph graph = null!;
private const int history_length = 89;
[SetUpSteps]
public void SetUpSteps()
{
RankGraph graph;
int[] data = new int[89];
int[] dataWithZeros = new int[89];
int[] smallData = new int[89];
int[] edgyData = new int[89];
for (int i = 0; i < 89; i++)
data[i] = dataWithZeros[i] = (i + 1) * 1000;
for (int i = 20; i < 60; i++)
dataWithZeros[i] = 0;
for (int i = 79; i < 89; i++)
smallData[i] = 100000 - i * 1000;
bool edge = true;
for (int i = 0; i < 20; i++)
{
edgyData[i] = 100000 + (edge ? 1000 : -1000) * (i + 1);
edge = !edge;
}
Add(new Container
AddStep("create graph", () => Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -67,34 +49,70 @@ namespace osu.Game.Tests.Visual.Online
}
}
});
}
[Test]
public void TestNullUser()
{
AddStep("null user", () => graph.Statistics.Value = null);
AddAssert("line graph hidden", () => this.ChildrenOfType<LineGraph>().All(graph => graph.Alpha == 0));
}
[Test]
public void TestRankOnly()
{
AddStep("rank only", () =>
{
graph.Statistics.Value = new UserStatistics
{
IsRanked = true,
GlobalRank = 123456,
PP = 12345,
};
});
AddAssert("line graph hidden", () => this.ChildrenOfType<LineGraph>().All(graph => graph.Alpha == 0));
}
[Test]
public void TestWithRankHistory()
{
int[] data = new int[history_length];
for (int i = 0; i < history_length; i++)
data[i] = (i + 1) * 1000;
AddStep("with rank history", () =>
{
graph.Statistics.Value = new UserStatistics
{
IsRanked = true,
GlobalRank = 89000,
PP = 12345,
RankHistory = new APIRankHistory
{
Data = data,
Data = data
}
};
});
AddAssert("line graph shown", () => this.ChildrenOfType<LineGraph>().All(graph => graph.Alpha == 1));
}
[Test]
public void TestRanksWithZeroValues()
{
int[] dataWithZeros = new int[history_length];
for (int i = 0; i < history_length; i++)
{
if (i < 20 || i >= 60)
dataWithZeros[i] = (i + 1) * 1000;
}
AddStep("with zero values", () =>
{
graph.Statistics.Value = new UserStatistics
{
IsRanked = true,
GlobalRank = 89000,
PP = 12345,
RankHistory = new APIRankHistory
@ -103,11 +121,22 @@ namespace osu.Game.Tests.Visual.Online
}
};
});
AddAssert("line graph shown", () => this.ChildrenOfType<LineGraph>().All(graph => graph.Alpha == 1));
}
[Test]
public void TestSmallAmountOfData()
{
int[] smallData = new int[history_length];
for (int i = history_length - 10; i < history_length; i++)
smallData[i] = 100000 - i * 1000;
AddStep("small amount of data", () =>
{
graph.Statistics.Value = new UserStatistics
{
IsRanked = true,
GlobalRank = 12000,
PP = 12345,
RankHistory = new APIRankHistory
@ -116,11 +145,27 @@ namespace osu.Game.Tests.Visual.Online
}
};
});
AddAssert("line graph shown", () => this.ChildrenOfType<LineGraph>().All(graph => graph.Alpha == 1));
}
[Test]
public void TestHistoryWithEdges()
{
int[] edgyData = new int[89];
bool edge = true;
for (int i = 0; i < 20; i++)
{
edgyData[i] = 100000 + (edge ? 1000 : -1000) * (i + 1);
edge = !edge;
}
AddStep("graph with edges", () =>
{
graph.Statistics.Value = new UserStatistics
{
IsRanked = true,
GlobalRank = 12000,
PP = 12345,
RankHistory = new APIRankHistory
@ -129,6 +174,7 @@ namespace osu.Game.Tests.Visual.Online
}
};
});
AddAssert("line graph shown", () => this.ChildrenOfType<LineGraph>().All(graph => graph.Alpha == 1));
}
}
}

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
@ -36,12 +37,16 @@ namespace osu.Game.Tests.Visual.Settings
Children = new Drawable[]
{
settings = new TabletSettings(tabletHandler)
new OsuScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.None,
Width = SettingsPanel.PANEL_WIDTH,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Child = settings = new TabletSettings(tabletHandler)
{
RelativeSizeAxes = Axes.None,
Width = SettingsPanel.PANEL_WIDTH,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
}
}
};
});

View File

@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@ -118,6 +119,15 @@ namespace osu.Game.Tests.Visual.SongSelect
}
}
[Test]
public void TestDeletion()
{
loadBeatmaps(count: 5, randomDifficulties: true);
AddStep("remove first set", () => carousel.RemoveBeatmapSet(carousel.Items.Select(item => item.Item).OfType<CarouselBeatmapSet>().First().BeatmapSet));
AddUntilStep("4 beatmap sets visible", () => this.ChildrenOfType<DrawableCarouselBeatmapSet>().Count(set => set.Alpha > 0) == 4);
}
[Test]
public void TestScrollPositionMaintainedOnDelete()
{

View File

@ -0,0 +1,178 @@
// 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.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public class TestSceneHistoryTextBox : OsuManualInputManagerTestScene
{
private const string temp = "Temp message";
private int messageCounter;
private HistoryTextBox box = null!;
private OsuSpriteText text = null!;
[SetUp]
public void SetUp()
{
Schedule(() =>
{
Children = new Drawable[]
{
box = new HistoryTextBox(5)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Width = 0.99f,
},
text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Width = 0.99f,
Y = -box.Height,
Font = OsuFont.Default.With(size: 20),
}
};
box.OnCommit += (_, _) =>
{
if (string.IsNullOrEmpty(box.Text))
return;
text.Text = $"{nameof(box.OnCommit)}: {box.Text}";
box.Text = string.Empty;
box.TakeFocus();
text.FadeOutFromOne(1000, Easing.InQuint);
};
messageCounter = 0;
box.TakeFocus();
});
}
[Test]
public void TestEmptyHistory()
{
AddStep("Set text", () => box.Text = temp);
AddStep("Move down", () => InputManager.Key(Key.Down));
AddAssert("Text is unchanged", () => box.Text == temp);
AddStep("Move up", () => InputManager.Key(Key.Up));
AddAssert("Text is unchanged", () => box.Text == temp);
}
[Test]
public void TestPartialHistory()
{
addMessages(3);
AddStep("Set text", () => box.Text = temp);
AddStep("Move down", () => InputManager.Key(Key.Down));
AddAssert("Text is unchanged", () => box.Text == temp);
AddRepeatStep("Move up", () => InputManager.Key(Key.Up), 3);
AddAssert("Same as 1st message", () => box.Text == "Message 1");
AddStep("Move up", () => InputManager.Key(Key.Up));
AddAssert("Same as 1st message", () => box.Text == "Message 1");
AddStep("Move down", () => InputManager.Key(Key.Down));
AddAssert("Same as 2nd message", () => box.Text == "Message 2");
AddRepeatStep("Move down", () => InputManager.Key(Key.Down), 2);
AddAssert("Temporary message restored", () => box.Text == temp);
AddStep("Move down", () => InputManager.Key(Key.Down));
AddAssert("Text is unchanged", () => box.Text == temp);
}
[Test]
public void TestFullHistory()
{
addMessages(7);
AddStep("Set text", () => box.Text = temp);
AddStep("Move down", () => InputManager.Key(Key.Down));
AddAssert("Text is unchanged", () => box.Text == temp);
AddRepeatStep("Move up", () => InputManager.Key(Key.Up), 5);
AddAssert("Same as 3rd message", () => box.Text == "Message 3");
AddStep("Move up", () => InputManager.Key(Key.Up));
AddAssert("Same as 3rd message", () => box.Text == "Message 3");
AddRepeatStep("Move down", () => InputManager.Key(Key.Down), 4);
AddAssert("Same as 7th message", () => box.Text == "Message 7");
AddStep("Move down", () => InputManager.Key(Key.Down));
AddAssert("Temporary message restored", () => box.Text == temp);
AddStep("Move down", () => InputManager.Key(Key.Down));
AddAssert("Text is unchanged", () => box.Text == temp);
}
[Test]
public void TestChangedHistory()
{
addMessages(2);
AddStep("Set text", () => box.Text = temp);
AddStep("Move up", () => InputManager.Key(Key.Up));
AddStep("Change text", () => box.Text = "New message");
AddStep("Move down", () => InputManager.Key(Key.Down));
AddStep("Move up", () => InputManager.Key(Key.Up));
AddAssert("Changes lost", () => box.Text == "Message 2");
}
[Test]
public void TestInputOnEdge()
{
addMessages(2);
AddStep("Set text", () => box.Text = temp);
AddStep("Move down", () => InputManager.Key(Key.Down));
AddAssert("Text unchanged", () => box.Text == temp);
AddRepeatStep("Move up", () => InputManager.Key(Key.Up), 2);
AddAssert("Same as 1st message", () => box.Text == "Message 1");
AddStep("Move up", () => InputManager.Key(Key.Up));
AddAssert("Text unchanged", () => box.Text == "Message 1");
}
[Test]
public void TestResetIndex()
{
addMessages(2);
AddRepeatStep("Move Up", () => InputManager.Key(Key.Up), 2);
AddAssert("Same as 1st message", () => box.Text == "Message 1");
AddStep("Change text", () => box.Text = "New message");
AddStep("Move Up", () => InputManager.Key(Key.Up));
AddAssert("Same as previous message", () => box.Text == "Message 2");
}
private void addMessages(int count)
{
AddRepeatStep("Add messages", () =>
{
box.Text = $"Message {++messageCounter}";
InputManager.Key(Key.Enter);
}, count);
}
}
}

View File

@ -136,7 +136,9 @@ namespace osu.Game.Beatmaps.Drawables
private static readonly string[] always_bundled_beatmaps =
{
// This thing is 40mb, I'm not sure we want it here...
@"1388906 Raphlesia & BilliumMoto - My Love.osz"
@"1388906 Raphlesia & BilliumMoto - My Love.osz",
// Winner of Triangles mapping competition: https://osu.ppy.sh/home/news/2022-10-06-results-triangles
@"1841885 cYsmix - triangles.osz",
};
private static readonly string[] bundled_osu =

View File

@ -147,7 +147,11 @@ namespace osu.Game.Beatmaps.Formats
);
}
protected string CleanFilename(string path) => path.Trim('"').ToStandardisedPath();
protected string CleanFilename(string path) => path
// User error which is supported by stable (https://github.com/ppy/osu/issues/21204)
.Replace(@"\\", @"\")
.Trim('"')
.ToStandardisedPath();
public enum Section
{

View File

@ -1,11 +1,11 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.IO;
@ -22,22 +22,42 @@ namespace osu.Game.Database
{
// make sure the directory exists
if (!storage.ExistsDirectory(string.Empty))
yield break;
return Array.Empty<string>();
foreach (string directory in storage.GetDirectories(string.Empty))
List<string> paths = new List<string>();
try
{
var directoryStorage = storage.GetStorageForDirectory(directory);
if (!directoryStorage.GetFiles(string.Empty).ExcludeSystemFileNames().Any())
foreach (string directory in storage.GetDirectories(string.Empty))
{
// if a directory doesn't contain files, attempt looking for beatmaps inside of that directory.
// this is a special behaviour in stable for beatmaps only, see https://github.com/ppy/osu/issues/18615.
foreach (string subDirectory in GetStableImportPaths(directoryStorage))
yield return subDirectory;
var directoryStorage = storage.GetStorageForDirectory(directory);
try
{
if (!directoryStorage.GetFiles(string.Empty).ExcludeSystemFileNames().Any())
{
// if a directory doesn't contain files, attempt looking for beatmaps inside of that directory.
// this is a special behaviour in stable for beatmaps only, see https://github.com/ppy/osu/issues/18615.
foreach (string subDirectory in GetStableImportPaths(directoryStorage))
paths.Add(subDirectory);
}
else
paths.Add(storage.GetFullPath(directory));
}
catch (Exception e)
{
// Catch any errors when enumerating files
Logger.Log($"Error when enumerating files in {directoryStorage.GetFullPath(string.Empty)}: {e}");
}
}
else
yield return storage.GetFullPath(directory);
}
catch (Exception e)
{
// Catch any errors when enumerating directories
Logger.Log($"Error when enumerating directories in {storage.GetFullPath(string.Empty)}: {e}");
}
return paths;
}
public LegacyBeatmapImporter(IModelImporter<BeatmapSetInfo> importer)

View File

@ -109,15 +109,11 @@ namespace osu.Game.Graphics.UserInterface
}
}
[Flags]
public enum BarDirection
{
LeftToRight = 1,
RightToLeft = 1 << 1,
TopToBottom = 1 << 2,
BottomToTop = 1 << 3,
Vertical = TopToBottom | BottomToTop,
Horizontal = LeftToRight | RightToLeft,
LeftToRight,
RightToLeft,
TopToBottom,
BottomToTop
}
}

View File

@ -5,15 +5,23 @@
using osuTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using System;
namespace osu.Game.Graphics.UserInterface
{
public class BarGraph : FillFlowContainer<Bar>
public class BarGraph : Drawable
{
private const int resize_duration = 250;
private const Easing easing = Easing.InOutCubic;
/// <summary>
/// Manually sets the max value, if null <see cref="Enumerable.Max(IEnumerable{float})"/> is instead used
/// </summary>
@ -21,22 +29,21 @@ namespace osu.Game.Graphics.UserInterface
private BarDirection direction = BarDirection.BottomToTop;
public new BarDirection Direction
public BarDirection Direction
{
get => direction;
set
{
direction = value;
base.Direction = direction.HasFlagFast(BarDirection.Horizontal) ? FillDirection.Vertical : FillDirection.Horizontal;
if (direction == value)
return;
foreach (var bar in Children)
{
bar.Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, 1.0f / Children.Count) : new Vector2(1.0f / Children.Count, 1);
bar.Direction = direction;
}
direction = value;
Invalidate(Invalidation.DrawNode);
}
}
private readonly BarsInfo bars = new BarsInfo();
/// <summary>
/// A list of floats that defines the length of each <see cref="Bar"/>
/// </summary>
@ -44,37 +51,199 @@ namespace osu.Game.Graphics.UserInterface
{
set
{
List<Bar> bars = Children.ToList();
foreach (var bar in value.Select((length, index) => new { Value = length, Bar = bars.Count > index ? bars[index] : null }))
if (!value.Any())
{
float length = MaxValue ?? value.Max();
if (length != 0)
length = bar.Value / length;
float size = value.Count();
if (size != 0)
size = 1.0f / size;
if (bar.Bar != null)
{
bar.Bar.Length = length;
bar.Bar.Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1);
}
else
{
Add(new Bar
{
RelativeSizeAxes = Axes.Both,
Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1),
Length = length,
Direction = Direction,
});
}
bars.Clear();
Invalidate(Invalidation.DrawNode);
return;
}
//I'm using ToList() here because Where() returns an Enumerable which can change it's elements afterwards
RemoveRange(Children.Where((_, index) => index >= value.Count()).ToList(), true);
float maxLength = MaxValue ?? value.Max();
bars.SetLengths(value.Select(v => maxLength == 0 ? 0 : Math.Max(0f, v / maxLength)).ToArray());
animationStartTime = Clock.CurrentTime;
animationComplete = false;
}
}
private double animationStartTime;
private bool animationComplete;
private IShader shader = null!;
private Texture texture = null!;
[BackgroundDependencyLoader]
private void load(IRenderer renderer, ShaderManager shaders)
{
texture = renderer.WhitePixel;
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
}
protected override void Update()
{
base.Update();
if (!bars.Any)
return;
double currentTime = Clock.CurrentTime;
if (currentTime < animationStartTime + resize_duration)
{
bars.Animate(animationStartTime, currentTime);
Invalidate(Invalidation.DrawNode);
}
else if (!animationComplete)
{
bars.FinishAnimation();
Invalidate(Invalidation.DrawNode);
animationComplete = true;
}
}
protected override DrawNode CreateDrawNode() => new BarGraphDrawNode(this);
private class BarGraphDrawNode : DrawNode
{
public new BarGraph Source => (BarGraph)base.Source;
public BarGraphDrawNode(BarGraph source)
: base(source)
{
}
private IShader shader = null!;
private Texture texture = null!;
private Vector2 drawSize;
private BarDirection direction;
private float barBreadth;
private readonly List<float> lengths = new List<float>();
public override void ApplyState()
{
base.ApplyState();
shader = Source.shader;
texture = Source.texture;
drawSize = Source.DrawSize;
direction = Source.direction;
barBreadth = Source.bars.Breadth;
lengths.Clear();
lengths.AddRange(Source.bars.InstantaneousLengths);
}
public override void Draw(IRenderer renderer)
{
base.Draw(renderer);
shader.Bind();
for (int i = 0; i < lengths.Count; i++)
{
float barHeight = drawSize.Y * ((direction == BarDirection.TopToBottom || direction == BarDirection.BottomToTop) ? lengths[i] : barBreadth);
float barWidth = drawSize.X * ((direction == BarDirection.LeftToRight || direction == BarDirection.RightToLeft) ? lengths[i] : barBreadth);
Vector2 topLeft;
switch (direction)
{
default:
case BarDirection.LeftToRight:
topLeft = new Vector2(0, i * barHeight);
break;
case BarDirection.RightToLeft:
topLeft = new Vector2(drawSize.X - barWidth, i * barHeight);
break;
case BarDirection.TopToBottom:
topLeft = new Vector2(i * barWidth, 0);
break;
case BarDirection.BottomToTop:
topLeft = new Vector2(i * barWidth, drawSize.Y - barHeight);
break;
}
renderer.DrawQuad(
texture,
new Quad(
Vector2Extensions.Transform(topLeft, DrawInfo.Matrix),
Vector2Extensions.Transform(topLeft + new Vector2(barWidth, 0), DrawInfo.Matrix),
Vector2Extensions.Transform(topLeft + new Vector2(0, barHeight), DrawInfo.Matrix),
Vector2Extensions.Transform(topLeft + new Vector2(barWidth, barHeight), DrawInfo.Matrix)
),
DrawColourInfo.Colour);
}
shader.Unbind();
}
}
private class BarsInfo
{
public bool Any => Count > 0;
public int Count { get; private set; }
public float Breadth { get; private set; }
public List<float> InstantaneousLengths { get; } = new List<float>();
private readonly List<float> initialLengths = new List<float>();
private readonly List<float> finalLengths = new List<float>();
public void Clear() => SetLengths(Array.Empty<float>());
public void SetLengths(float[] newLengths)
{
int newCount = newLengths.Length;
for (int i = 0; i < newCount; i++)
{
// If we have an old bar at this index - change it's length
if (i < Count)
{
initialLengths[i] = finalLengths[i];
finalLengths[i] = newLengths[i];
continue;
}
// If exceeded old bars count - add new one
initialLengths.Add(0);
finalLengths.Add(newLengths[i]);
InstantaneousLengths.Add(0);
}
// Remove excessive bars
if (Count > newCount)
{
int barsToRemove = Count - newCount;
initialLengths.RemoveRange(newCount, barsToRemove);
finalLengths.RemoveRange(newCount, barsToRemove);
InstantaneousLengths.RemoveRange(newCount, barsToRemove);
}
Count = newCount;
Breadth = Count == 0 ? 0 : (1f / Count);
}
public void Animate(double animationStartTime, double currentTime)
{
for (int i = 0; i < Count; i++)
InstantaneousLengths[i] = Interpolation.ValueAt(currentTime, initialLengths[i], finalLengths[i], animationStartTime, animationStartTime + resize_duration, easing);
}
public void FinishAnimation()
{
for (int i = 0; i < Count; i++)
InstantaneousLengths[i] = finalLengths[i];
}
}
}

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 osu.Framework.Input.Events;
using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Graphics.UserInterface
{
/// <summary>
/// A <see cref="FocusedTextBox"/> which additionally retains a history of text committed, up to a limit
/// (100 by default, specified in constructor).
/// The history of committed text can be navigated using up/down arrows.
/// This resembles the operation of command-line terminals.
/// </summary>
public class HistoryTextBox : FocusedTextBox
{
private readonly LimitedCapacityQueue<string> messageHistory;
public int HistoryCount => messageHistory.Count;
private int selectedIndex;
private string originalMessage = string.Empty;
/// <summary>
/// Creates a new <see cref="HistoryTextBox"/>.
/// </summary>
/// <param name="capacity">
/// The maximum number of committed lines to keep in history.
/// When exceeded, the oldest lines in history will be dropped to make space for new ones.
/// </param>
public HistoryTextBox(int capacity = 100)
{
messageHistory = new LimitedCapacityQueue<string>(capacity);
Current.ValueChanged += text =>
{
if (selectedIndex != HistoryCount && text.NewValue != messageHistory[selectedIndex])
{
selectedIndex = HistoryCount;
}
};
}
protected override bool OnKeyDown(KeyDownEvent e)
{
switch (e.Key)
{
case Key.Up:
if (selectedIndex == 0)
return true;
if (selectedIndex == HistoryCount)
originalMessage = Text;
Text = messageHistory[--selectedIndex];
return true;
case Key.Down:
if (selectedIndex == HistoryCount)
return true;
if (selectedIndex == HistoryCount - 1)
{
selectedIndex = HistoryCount;
Text = originalMessage;
return true;
}
Text = messageHistory[++selectedIndex];
return true;
}
return base.OnKeyDown(e);
}
protected override void Commit()
{
if (!string.IsNullOrEmpty(Text))
messageHistory.Enqueue(Text);
selectedIndex = HistoryCount;
base.Commit();
}
}
}

View File

@ -16,6 +16,8 @@ namespace osu.Game.Online.API.Requests
{
public class GetScoresRequest : APIRequest<APIScoresCollection>
{
public const int MAX_SCORES_PER_REQUEST = 50;
private readonly IBeatmapInfo beatmapInfo;
private readonly BeatmapLeaderboardScope scope;
private readonly IRulesetInfo ruleset;

View File

@ -120,17 +120,20 @@ namespace osu.Game.Online.Chat
AddInternal(drawableChannel);
}
public class ChatTextBox : FocusedTextBox
public class ChatTextBox : HistoryTextBox
{
protected override bool OnKeyDown(KeyDownEvent e)
{
// Chat text boxes are generally used in places where they retain focus, but shouldn't block interaction with other
// elements on the same screen.
switch (e.Key)
if (!HoldFocus)
{
case Key.Up:
case Key.Down:
return false;
switch (e.Key)
{
case Key.Up:
case Key.Down:
return false;
}
}
return base.OnKeyDown(e);

View File

@ -7,7 +7,7 @@ using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Chat
{
public class ChatTextBox : FocusedTextBox
public class ChatTextBox : HistoryTextBox
{
public readonly BindableBool ShowSearch = new BindableBool();

View File

@ -18,6 +18,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
using osuTK;
namespace osu.Game.Overlays.FirstRunSetup
@ -26,7 +27,7 @@ namespace osu.Game.Overlays.FirstRunSetup
public class ScreenWelcome : FirstRunSetupScreen
{
[BackgroundDependencyLoader]
private void load()
private void load(FrameworkConfigManager frameworkConfig)
{
Content.Children = new Drawable[]
{
@ -52,6 +53,11 @@ namespace osu.Game.Overlays.FirstRunSetup
},
}
},
new SettingsCheckbox
{
LabelText = GeneralSettingsStrings.PreferOriginalMetadataLanguage,
Current = frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowUnicode)
},
new LanguageSelectionFlow
{
RelativeSizeAxes = Axes.X,

View File

@ -59,7 +59,11 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
// the dropdown. BASS does not give us a simple mechanism to select
// specific audio devices in such a case anyways. Such
// functionality would require involved OS-specific code.
dropdown.Items = deviceItems.Distinct().ToList();
dropdown.Items = deviceItems
// Dropdown doesn't like null items. Somehow we are seeing some arrive here (see https://github.com/ppy/osu/issues/21271)
.Where(i => i != null)
.Distinct()
.ToList();
}
protected override void Dispose(bool isDisposing)

View File

@ -125,11 +125,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
usableAreaContainer.ResizeTo(val.NewValue, 100, Easing.OutQuint);
int x = (int)val.NewValue.X;
int y = (int)val.NewValue.Y;
int x = (int)Math.Round(val.NewValue.X);
int y = (int)Math.Round(val.NewValue.Y);
int commonDivider = greatestCommonDivider(x, y);
usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}";
usableAreaText.Text = $"{x / commonDivider}:{y / commonDivider}";
checkBounds();
}, true);

View File

@ -3,7 +3,6 @@
#nullable disable
using System;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -46,9 +45,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private GameHost host { get; set; }
/// <summary>
/// Based on ultrawide monitor configurations.
/// Based on ultrawide monitor configurations, plus a bit of lenience for users which are intentionally aiming for higher horizontal velocity.
/// </summary>
private const float largest_feasible_aspect_ratio = 21f / 9;
private const float largest_feasible_aspect_ratio = 23f / 9;
private readonly BindableNumber<float> aspectRatio = new BindableFloat(1)
{
@ -327,7 +326,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
aspectLock.Value = false;
int proposedHeight = getHeight(sizeX.Value, aspectRatio);
float proposedHeight = getHeight(sizeX.Value, aspectRatio);
if (proposedHeight < sizeY.MaxValue)
sizeY.Value = proposedHeight;
@ -344,8 +343,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private float currentAspectRatio => sizeX.Value / sizeY.Value;
private static int getHeight(float width, float aspectRatio) => (int)Math.Round(width / aspectRatio);
private static float getHeight(float width, float aspectRatio) => width / aspectRatio;
private static int getWidth(float height, float aspectRatio) => (int)Math.Round(height * aspectRatio);
private static float getWidth(float height, float aspectRatio) => height * aspectRatio;
}
}

View File

@ -89,6 +89,10 @@ namespace osu.Game
// check if we are already at a valid target screen.
if (validScreens.Any(t => t.IsAssignableFrom(type)))
{
if (!((Drawable)current).IsLoaded)
// wait until screen is loaded before invoking action.
return true;
finalAction(current);
Cancel();
return true;

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// A type of <see cref="DrawableRuleset{TObject}"/> that supports a <see cref="ScrollingPlayfield"/>.
/// <see cref="HitObject"/>s inside this <see cref="DrawableRuleset{TObject}"/> will scroll within the playfield.
/// </summary>
public abstract class DrawableScrollingRuleset<TObject> : DrawableRuleset<TObject>, IKeyBindingHandler<GlobalAction>
public abstract class DrawableScrollingRuleset<TObject> : DrawableRuleset<TObject>, IDrawableScrollingRuleset, IKeyBindingHandler<GlobalAction>
where TObject : HitObject
{
/// <summary>
@ -66,6 +66,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
protected virtual ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Sequential;
ScrollVisualisationMethod IDrawableScrollingRuleset.VisualisationMethod => VisualisationMethod;
/// <summary>
/// Whether the player can change <see cref="TimeRange"/>.
/// </summary>

View File

@ -0,0 +1,15 @@
// 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.Game.Configuration;
namespace osu.Game.Rulesets.UI.Scrolling
{
/// <summary>
/// An interface for scrolling-based <see cref="DrawableRuleset{TObject}"/>s.
/// </summary>
public interface IDrawableScrollingRuleset
{
ScrollVisualisationMethod VisualisationMethod { get; }
}
}

View File

@ -296,6 +296,13 @@ namespace osu.Game.Scoring
break;
}
case HitResult.LargeBonus:
case HitResult.SmallBonus:
if (MaximumStatistics.TryGetValue(r.result, out int count) && count > 0)
yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName);
break;
case HitResult.SmallTickMiss:
case HitResult.LargeTickMiss:
break;

View File

@ -270,7 +270,7 @@ namespace osu.Game.Screens.Edit
{
IsSeeking &= Transforms.Any();
if (track.Value?.IsRunning != true)
if (!IsRunning)
{
// seeking in the editor can happen while the track isn't running.
// in this case we always want to expose ourselves as seeking (to avoid sample playback).

View File

@ -74,7 +74,6 @@ namespace osu.Game.Screens.Edit.GameplayTest
{
musicController.Stop();
editorState.Time = GameplayClockContainer.CurrentTime;
editor.RestoreState(editorState);
return base.OnExiting(e);
}

View File

@ -7,7 +7,9 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Screens.Edit.Timing
{
@ -41,6 +43,10 @@ namespace osu.Game.Screens.Edit.Timing
omitBarLine.Current.BindValueChanged(_ => saveChanges());
scrollSpeedSlider.Current.BindValueChanged(_ => saveChanges());
var drawableRuleset = Beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateDrawableRulesetWith(Beatmap.PlayableBeatmap);
if (drawableRuleset is not IDrawableScrollingRuleset scrollingRuleset || scrollingRuleset.VisualisationMethod == ScrollVisualisationMethod.Constant)
scrollSpeedSlider.Hide();
void saveChanges()
{
if (!isRebinding) ChangeHandler?.SaveState();

View File

@ -171,12 +171,14 @@ namespace osu.Game.Screens.Play.HUD
for (int i = 0; i < Flow.Count; i++)
{
Flow.SetLayoutPosition(orderedByScore[i], i);
orderedByScore[i].ScorePosition = i + 1;
orderedByScore[i].ScorePosition = CheckValidScorePosition(i + 1) ? i + 1 : null;
}
sorting.Validate();
}
protected virtual bool CheckValidScorePosition(int i) => true;
private class InputDisabledScrollContainer : OsuScrollContainer
{
public InputDisabledScrollContainer()

View File

@ -62,20 +62,22 @@ namespace osu.Game.Screens.Play.HUD
private int? scorePosition;
private bool scorePositionIsSet;
public int? ScorePosition
{
get => scorePosition;
set
{
if (value == scorePosition)
// We always want to run once, as the incoming value may be null and require a visual update to "-".
if (value == scorePosition && scorePositionIsSet)
return;
scorePosition = value;
if (scorePosition.HasValue)
positionText.Text = $"#{scorePosition.Value.FormatRank()}";
positionText.Text = scorePosition.HasValue ? $"#{scorePosition.Value.FormatRank()}" : "-";
scorePositionIsSet = true;
positionText.FadeTo(scorePosition.HasValue ? 1 : 0);
updateState();
}
}

View File

@ -7,8 +7,10 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Select;
using osu.Game.Users;
namespace osu.Game.Screens.Play.HUD
@ -18,6 +20,9 @@ namespace osu.Game.Screens.Play.HUD
private const int duration = 100;
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
private readonly Bindable<PlayBeatmapDetailArea.TabType> scoreSource = new Bindable<PlayBeatmapDetailArea.TabType>();
private readonly IUser trackingUser;
public readonly IBindableList<ScoreInfo> Scores = new BindableList<ScoreInfo>();
@ -46,11 +51,13 @@ namespace osu.Game.Screens.Play.HUD
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
config.BindWith(OsuSetting.BeatmapDetailTab, scoreSource);
}
protected override void LoadComplete()
{
base.LoadComplete();
Scores.BindCollectionChanged((_, _) => Scheduler.AddOnce(showScores), true);
// Alpha will be updated via `updateVisibility` below.
@ -93,6 +100,18 @@ namespace osu.Game.Screens.Play.HUD
local.DisplayOrder.Value = long.MaxValue;
}
protected override bool CheckValidScorePosition(int i)
{
// change displayed position to '-' when there are 50 already submitted scores and tracked score is last
if (scoreSource.Value != PlayBeatmapDetailArea.TabType.Local)
{
if (i == Flow.Count && Flow.Count > GetScoresRequest.MAX_SCORES_PER_REQUEST)
return false;
}
return base.CheckValidScorePosition(i);
}
private void updateVisibility() =>
this.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration);
}

View File

@ -770,7 +770,7 @@ namespace osu.Game.Screens.Select
{
updateItem(item);
if (!item.Item.Filtered.Value)
if (item.Item.Visible)
{
bool isSelected = item.Item.State.Value == CarouselItemState.Selected;

View File

@ -204,10 +204,11 @@ namespace osu.Game.Screens.Select.Leaderboards
}
else if (filterMods)
{
// otherwise find all the scores that have *any* of the currently selected mods (similar to how web applies mod filters)
// we're creating and using a string list representation of selected mods so that it can be translated into the DB query itself
var selectedMods = mods.Value.Select(m => m.Acronym);
scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym)));
// otherwise find all the scores that have all of the currently selected mods (similar to how web applies mod filters)
// we're creating and using a string HashSet representation of selected mods so that it can be translated into the DB query itself
var selectedMods = mods.Value.Select(m => m.Acronym).ToHashSet();
scores = scores.Where(s => selectedMods.SetEquals(s.Mods.Select(m => m.Acronym)));
}
scores = scoreManager.OrderByTotalScore(scores.Detach());

View File

@ -304,6 +304,16 @@ namespace osu.Game.Screens.Select
modSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(ModSelect);
}
protected override bool OnScroll(ScrollEvent e)
{
// Match stable behaviour of only alt-scroll adjusting volume.
// Supporting scroll adjust without a modifier key just feels bad, since there are so many scrollable elements on the screen.
if (!e.CurrentState.Keyboard.AltPressed)
return true;
return base.OnScroll(e);
}
/// <summary>
/// Creates the buttons to be displayed in the footer.
/// </summary>

View File

@ -7,7 +7,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Difficulty.Utils
namespace osu.Game.Utils
{
/// <summary>
/// An indexed queue with limited capacity.