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

Merge branch 'master' into limit-barline-generation-sanity

This commit is contained in:
Bartłomiej Dach 2022-11-23 17:12:36 +01:00 committed by GitHub
commit 508f1f488c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 922 additions and 178 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

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

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

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

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

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

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

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

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

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

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