Merge branch 'master' into slider-control-point-no-distance-snap

This commit is contained in:
Bartłomiej Dach 2023-05-26 19:52:54 +02:00
commit 53c91349fe
No known key found for this signature in database
31 changed files with 630 additions and 305 deletions

View File

@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
if (slider is null) return; if (slider is null) return;
sample = new HitSampleInfo("hitwhistle", "soft", volume: 70); sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70);
slider.Samples.Add(sample.With()); slider.Samples.Add(sample.With());
}); });

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -13,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Scoring; using osu.Game.Scoring;
using osuTK; using osuTK;
@ -36,8 +35,8 @@ namespace osu.Game.Rulesets.Osu.Statistics
private const float rotation = 45; private const float rotation = 45;
private BufferedContainer bufferedGrid; private BufferedContainer bufferedGrid = null!;
private GridContainer pointGrid; private GridContainer pointGrid = null!;
private readonly ScoreInfo score; private readonly ScoreInfo score;
private readonly IBeatmap playableBeatmap; private readonly IBeatmap playableBeatmap;
@ -58,6 +57,8 @@ namespace osu.Game.Rulesets.Osu.Statistics
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
const float line_extension = 0.2f;
InternalChild = new Container InternalChild = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -66,76 +67,99 @@ namespace osu.Game.Rulesets.Osu.Statistics
FillMode = FillMode.Fit, FillMode = FillMode.Fit,
Children = new Drawable[] Children = new Drawable[]
{ {
new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(inner_portion),
Masking = true,
BorderThickness = line_thickness,
BorderColour = Color4.White,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#202624")
}
},
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(inner_portion),
Masking = true,
BorderThickness = line_thickness,
BorderColour = Color4.White,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#202624")
}
},
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(1), Padding = new MarginPadding(1),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Rotation = rotation,
Child = new Container Child = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[] Children = new Drawable[]
{ {
new Box new Circle
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
EdgeSmoothness = new Vector2(1),
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Height = 2, // We're rotating along a diagonal - we don't really care how big this is. Width = line_thickness,
Width = line_thickness / 2, Height = inner_portion + line_extension,
Rotation = -rotation, Rotation = -rotation * 2,
Alpha = 0.3f, Alpha = 0.6f,
}, },
new Box new Circle
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
EdgeSmoothness = new Vector2(1),
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Height = 2, // We're rotating along a diagonal - we don't really care how big this is. Width = line_thickness,
Width = line_thickness / 2, // adjust for edgesmoothness Height = inner_portion + line_extension,
Rotation = rotation
}, },
new OsuSpriteText
{
Text = "Overshoot",
Anchor = Anchor.Centre,
Origin = Anchor.BottomCentre,
Padding = new MarginPadding(3),
RelativePositionAxes = Axes.Both,
Y = -(inner_portion + line_extension) / 2,
},
new OsuSpriteText
{
Text = "Undershoot",
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
Padding = new MarginPadding(3),
RelativePositionAxes = Axes.Both,
Y = (inner_portion + line_extension) / 2,
},
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.Both,
Y = -(inner_portion + line_extension) / 2,
Margin = new MarginPadding(-line_thickness / 2),
Width = line_thickness,
Height = 10,
Rotation = 45,
},
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.Both,
Y = -(inner_portion + line_extension) / 2,
Margin = new MarginPadding(-line_thickness / 2),
Width = line_thickness,
Height = 10,
Rotation = -45,
}
} }
}, },
}, },
new Box
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Width = 10,
EdgeSmoothness = new Vector2(1),
Height = line_thickness / 2, // adjust for edgesmoothness
},
new Box
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
EdgeSmoothness = new Vector2(1),
Width = line_thickness / 2, // adjust for edgesmoothness
Height = 10,
}
} }
}, },
bufferedGrid = new BufferedContainer(cachedFrameBuffer: true) bufferedGrid = new BufferedContainer(cachedFrameBuffer: true)

View File

@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
StartTime = 100, StartTime = 100,
Samples = new List<HitSampleInfo> Samples = new List<HitSampleInfo>
{ {
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft") new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT)
} }
}; };
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@ -100,13 +100,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
}); });
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft"); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft"); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
AddStep("seek past hit", () => manualClock.CurrentTime = 200); AddStep("seek past hit", () => manualClock.CurrentTime = 200);
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft"); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft"); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
} }
[Test] [Test]
@ -183,7 +183,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
EndTime = 1100, EndTime = 1100,
Samples = new List<HitSampleInfo> Samples = new List<HitSampleInfo>
{ {
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft") new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT)
} }
}; };
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@ -192,18 +192,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
}); });
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft"); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft"); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600); AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600);
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft"); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft"); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200); AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200);
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft"); checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft"); checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
} }
[Test] [Test]

View File

@ -253,17 +253,17 @@ namespace osu.Game.Tests.Beatmaps.Formats
var soundPoint = controlPoints.SamplePointAt(0); var soundPoint = controlPoints.SamplePointAt(0);
Assert.AreEqual(956, soundPoint.Time); Assert.AreEqual(956, soundPoint.Time);
Assert.AreEqual("soft", soundPoint.SampleBank); Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
Assert.AreEqual(60, soundPoint.SampleVolume); Assert.AreEqual(60, soundPoint.SampleVolume);
soundPoint = controlPoints.SamplePointAt(53373); soundPoint = controlPoints.SamplePointAt(53373);
Assert.AreEqual(53373, soundPoint.Time); Assert.AreEqual(53373, soundPoint.Time);
Assert.AreEqual("soft", soundPoint.SampleBank); Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
Assert.AreEqual(60, soundPoint.SampleVolume); Assert.AreEqual(60, soundPoint.SampleVolume);
soundPoint = controlPoints.SamplePointAt(119637); soundPoint = controlPoints.SamplePointAt(119637);
Assert.AreEqual(119637, soundPoint.Time); Assert.AreEqual(119637, soundPoint.Time);
Assert.AreEqual("soft", soundPoint.SampleBank); Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
Assert.AreEqual(80, soundPoint.SampleVolume); Assert.AreEqual(80, soundPoint.SampleVolume);
var effectPoint = controlPoints.EffectPointAt(0); var effectPoint = controlPoints.EffectPointAt(0);
@ -305,10 +305,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(controlPoints.EffectPointAt(2500).KiaiMode, Is.False); Assert.That(controlPoints.EffectPointAt(2500).KiaiMode, Is.False);
Assert.That(controlPoints.EffectPointAt(3500).KiaiMode, Is.True); Assert.That(controlPoints.EffectPointAt(3500).KiaiMode, Is.True);
Assert.That(controlPoints.SamplePointAt(500).SampleBank, Is.EqualTo("drum")); Assert.That(controlPoints.SamplePointAt(500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM));
Assert.That(controlPoints.SamplePointAt(1500).SampleBank, Is.EqualTo("drum")); Assert.That(controlPoints.SamplePointAt(1500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM));
Assert.That(controlPoints.SamplePointAt(2500).SampleBank, Is.EqualTo("normal")); Assert.That(controlPoints.SamplePointAt(2500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_NORMAL));
Assert.That(controlPoints.SamplePointAt(3500).SampleBank, Is.EqualTo("drum")); Assert.That(controlPoints.SamplePointAt(3500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM));
Assert.That(controlPoints.TimingPointAt(500).BeatLength, Is.EqualTo(500).Within(0.1)); Assert.That(controlPoints.TimingPointAt(500).BeatLength, Is.EqualTo(500).Within(0.1));
Assert.That(controlPoints.TimingPointAt(1500).BeatLength, Is.EqualTo(500).Within(0.1)); Assert.That(controlPoints.TimingPointAt(1500).BeatLength, Is.EqualTo(500).Within(0.1));

View File

@ -13,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
@ -52,7 +53,7 @@ namespace osu.Game.Tests.Visual.Editing
Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2, Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2,
Samples = new List<HitSampleInfo> Samples = new List<HitSampleInfo>
{ {
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft", volume: 60) new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT, volume: 60)
} }
}); });
}); });
@ -67,14 +68,14 @@ namespace osu.Game.Tests.Visual.Editing
hitObjectHasSampleBank(0, "normal"); hitObjectHasSampleBank(0, "normal");
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
hitObjectHasSampleBank(1, "soft"); hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
AddStep("remove clap addition", () => InputManager.Key(Key.R)); AddStep("remove clap addition", () => InputManager.Key(Key.R));
hitObjectHasSampleBank(0, "normal"); hitObjectHasSampleBank(0, "normal");
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL); hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleBank(1, "soft"); hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL); hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
} }
@ -89,7 +90,7 @@ namespace osu.Game.Tests.Visual.Editing
public void TestSingleSelection() public void TestSingleSelection()
{ {
clickSamplePiece(0); clickSamplePiece(0);
samplePopoverHasSingleBank("normal"); samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL);
samplePopoverHasSingleVolume(80); samplePopoverHasSingleVolume(80);
dismissPopover(); dismissPopover();
@ -99,21 +100,21 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First())); AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First()));
clickSamplePiece(1); clickSamplePiece(1);
samplePopoverHasSingleBank("soft"); samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
samplePopoverHasSingleVolume(60); samplePopoverHasSingleVolume(60);
setVolumeViaPopover(90); setVolumeViaPopover(90);
hitObjectHasSampleVolume(1, 90); hitObjectHasSampleVolume(1, 90);
setBankViaPopover("drum"); setBankViaPopover(HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(1, "drum"); hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
} }
[Test] [Test]
public void TestUndo() public void TestUndo()
{ {
clickSamplePiece(1); clickSamplePiece(1);
samplePopoverHasSingleBank("soft"); samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
samplePopoverHasSingleVolume(60); samplePopoverHasSingleVolume(60);
setVolumeViaPopover(90); setVolumeViaPopover(90);
@ -170,7 +171,7 @@ namespace osu.Game.Tests.Visual.Editing
} }
[Test] [Test]
public void TestMultipleSelectionWithSameSampleBank() public void TestPopoverMultipleSelectionWithSameSampleBank()
{ {
AddStep("unify sample bank", () => AddStep("unify sample bank", () =>
{ {
@ -178,33 +179,33 @@ namespace osu.Game.Tests.Visual.Editing
{ {
for (int i = 0; i < h.Samples.Count; i++) for (int i = 0; i < h.Samples.Count; i++)
{ {
h.Samples[i] = h.Samples[i].With(newBank: "soft"); h.Samples[i] = h.Samples[i].With(newBank: HitSampleInfo.BANK_SOFT);
} }
} }
}); });
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0); clickSamplePiece(0);
samplePopoverHasSingleBank("soft"); samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
dismissPopover(); dismissPopover();
clickSamplePiece(1); clickSamplePiece(1);
samplePopoverHasSingleBank("soft"); samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
setBankViaPopover(string.Empty); setBankViaPopover(string.Empty);
hitObjectHasSampleBank(0, "soft"); hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleBank(1, "soft"); hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
samplePopoverHasSingleBank("soft"); samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
setBankViaPopover("drum"); setBankViaPopover(HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(0, "drum"); hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(1, "drum"); hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
samplePopoverHasSingleBank("drum"); samplePopoverHasSingleBank(HitSampleInfo.BANK_DRUM);
} }
[Test] [Test]
public void TestMultipleSelectionWithDifferentSampleBank() public void TestPopoverMultipleSelectionWithDifferentSampleBank()
{ {
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0); clickSamplePiece(0);
@ -216,14 +217,109 @@ namespace osu.Game.Tests.Visual.Editing
samplePopoverHasIndeterminateBank(); samplePopoverHasIndeterminateBank();
setBankViaPopover(string.Empty); setBankViaPopover(string.Empty);
hitObjectHasSampleBank(0, "normal"); hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(1, "soft"); hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
samplePopoverHasIndeterminateBank(); samplePopoverHasIndeterminateBank();
setBankViaPopover("normal"); setBankViaPopover(HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(0, "normal"); hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(1, "normal"); hitObjectHasSampleBank(1, HitSampleInfo.BANK_NORMAL);
samplePopoverHasSingleBank("normal"); samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL);
}
[Test]
public void TestHotkeysMultipleSelectionWithSameSampleBank()
{
AddStep("unify sample bank", () =>
{
foreach (var h in EditorBeatmap.HitObjects)
{
for (int i = 0; i < h.Samples.Count; i++)
{
h.Samples[i] = h.Samples[i].With(newBank: HitSampleInfo.BANK_SOFT);
}
}
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
AddStep("Press normal bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.W);
InputManager.ReleaseKey(Key.ShiftLeft);
});
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_NORMAL);
AddStep("Press drum bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ShiftLeft);
});
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
AddStep("Press auto bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.Q);
InputManager.ReleaseKey(Key.ShiftLeft);
});
// Should be a noop.
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
}
[Test]
public void TestHotkeysDuringPlacement()
{
AddStep("Enter placement mode", () => InputManager.Key(Key.Number2));
AddStep("Move mouse to centre", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<HitObjectComposer>().First().ScreenSpaceDrawQuad.Centre));
AddStep("Move between two objects", () => EditorClock.Seek(250));
AddStep("Press normal bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.W);
InputManager.ReleaseKey(Key.ShiftLeft);
});
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
AddStep("Press drum bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ShiftLeft);
});
checkPlacementSample(HitSampleInfo.BANK_DRUM);
AddStep("Press auto bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.Q);
InputManager.ReleaseKey(Key.ShiftLeft);
});
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
AddStep("Move after second object", () => EditorClock.Seek(750));
checkPlacementSample(HitSampleInfo.BANK_SOFT);
AddStep("Move to first object", () => EditorClock.Seek(0));
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected));
} }
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () => private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>

View File

@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay
new HitCircle new HitCircle
{ {
StartTime = t += spacing, StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft") }, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) },
}, },
new HitCircle new HitCircle
{ {
@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
StartTime = t += spacing, StartTime = t += spacing,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }), Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, "soft") }, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) },
}, },
}); });

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
@ -137,8 +138,7 @@ namespace osu.Game.Tests.Visual.UserInterface
if (!objects.Any()) if (!objects.Any())
return; return;
double firstHit = objects.First().StartTime; (double firstHit, double lastHit) = BeatmapExtensions.CalculatePlayableBounds(objects);
double lastHit = objects.Max(o => o.GetEndTime());
if (lastHit == 0) if (lastHit == 0)
lastHit = objects.Last().StartTime; lastHit = objects.Last().StartTime;

View File

@ -20,11 +20,20 @@ namespace osu.Game.Audio
public const string HIT_FINISH = @"hitfinish"; public const string HIT_FINISH = @"hitfinish";
public const string HIT_CLAP = @"hitclap"; public const string HIT_CLAP = @"hitclap";
public const string BANK_NORMAL = @"normal";
public const string BANK_SOFT = @"soft";
public const string BANK_DRUM = @"drum";
/// <summary> /// <summary>
/// All valid sample addition constants. /// All valid sample addition constants.
/// </summary> /// </summary>
public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP }; public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP };
/// <summary>
/// All valid bank constants.
/// </summary>
public static IEnumerable<string> AllBanks => new[] { BANK_NORMAL, BANK_SOFT, BANK_DRUM };
/// <summary> /// <summary>
/// The name of the sample to load. /// The name of the sample to load.
/// </summary> /// </summary>

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging; using osu.Framework.Logging;
@ -11,7 +10,6 @@ using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -74,7 +72,7 @@ namespace osu.Game.Beatmaps
var calculator = ruleset.CreateDifficultyCalculator(working); var calculator = ruleset.CreateDifficultyCalculator(working);
beatmap.StarRating = calculator.Calculate().StarRating; beatmap.StarRating = calculator.Calculate().StarRating;
beatmap.Length = calculateLength(working.Beatmap); beatmap.Length = working.Beatmap.CalculatePlayableLength();
beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength(); beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength();
} }
@ -82,20 +80,6 @@ namespace osu.Game.Beatmaps
workingBeatmapCache.Invalidate(beatmapSet); workingBeatmapCache.Invalidate(beatmapSet);
}); });
private double calculateLength(IBeatmap b)
{
if (!b.HitObjects.Any())
return 0;
var lastObject = b.HitObjects.Last();
//TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list).
double endTime = lastObject.GetEndTime();
double startTime = b.HitObjects.First().StartTime;
return endTime - startTime;
}
#region Implementation of IDisposable #region Implementation of IDisposable
public void Dispose() public void Dispose()

View File

@ -14,7 +14,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </remarks> /// </remarks>
public class SampleControlPoint : ControlPoint, IEquatable<SampleControlPoint> public class SampleControlPoint : ControlPoint, IEquatable<SampleControlPoint>
{ {
public const string DEFAULT_BANK = "normal"; public const string DEFAULT_BANK = HitSampleInfo.BANK_NORMAL;
public static readonly SampleControlPoint DEFAULT = new SampleControlPoint public static readonly SampleControlPoint DEFAULT = new SampleControlPoint
{ {

View File

@ -12,6 +12,7 @@ using System.Linq;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
@ -480,7 +481,7 @@ namespace osu.Game.Beatmaps.Formats
string stringSampleSet = sampleSet.ToString().ToLowerInvariant(); string stringSampleSet = sampleSet.ToString().ToLowerInvariant();
if (stringSampleSet == @"none") if (stringSampleSet == @"none")
stringSampleSet = @"normal"; stringSampleSet = HitSampleInfo.BANK_NORMAL;
if (timingChange) if (timingChange)
{ {

View File

@ -589,13 +589,13 @@ namespace osu.Game.Beatmaps.Formats
{ {
switch (sampleBank?.ToLowerInvariant()) switch (sampleBank?.ToLowerInvariant())
{ {
case "normal": case HitSampleInfo.BANK_NORMAL:
return LegacySampleBank.Normal; return LegacySampleBank.Normal;
case "soft": case HitSampleInfo.BANK_SOFT:
return LegacySampleBank.Soft; return LegacySampleBank.Soft;
case "drum": case HitSampleInfo.BANK_DRUM:
return LegacySampleBank.Drum; return LegacySampleBank.Drum;
default: default:

View File

@ -104,6 +104,19 @@ namespace osu.Game.Beatmaps
} }
} }
/// <summary>
/// Find the total milliseconds between the first and last hittable objects.
/// </summary>
/// <remarks>
/// This is cached to <see cref="BeatmapInfo.Length"/>, so using that is preferable when available.
/// </remarks>
public static double CalculatePlayableLength(this IBeatmap beatmap) => CalculatePlayableLength(beatmap.HitObjects);
/// <summary>
/// Find the timestamps in milliseconds of the start and end of the playable region.
/// </summary>
public static (double start, double end) CalculatePlayableBounds(this IBeatmap beatmap) => CalculatePlayableBounds(beatmap.HitObjects);
/// <summary> /// <summary>
/// Find the absolute end time of the latest <see cref="HitObject"/> in a beatmap. Will throw if beatmap contains no objects. /// Find the absolute end time of the latest <see cref="HitObject"/> in a beatmap. Will throw if beatmap contains no objects.
/// </summary> /// </summary>
@ -114,5 +127,36 @@ namespace osu.Game.Beatmaps
/// It's not super efficient so calls should be kept to a minimum. /// It's not super efficient so calls should be kept to a minimum.
/// </remarks> /// </remarks>
public static double GetLastObjectTime(this IBeatmap beatmap) => beatmap.HitObjects.Max(h => h.GetEndTime()); public static double GetLastObjectTime(this IBeatmap beatmap) => beatmap.HitObjects.Max(h => h.GetEndTime());
#region Helper methods
/// <summary>
/// Find the total milliseconds between the first and last hittable objects.
/// </summary>
/// <remarks>
/// This is cached to <see cref="BeatmapInfo.Length"/>, so using that is preferable when available.
/// </remarks>
public static double CalculatePlayableLength(IEnumerable<HitObject> objects)
{
(double start, double end) = CalculatePlayableBounds(objects);
return end - start;
}
/// <summary>
/// Find the timestamps in milliseconds of the start and end of the playable region.
/// </summary>
public static (double start, double end) CalculatePlayableBounds(IEnumerable<HitObject> objects)
{
if (!objects.Any())
return (0, 0);
double lastObjectTime = objects.Max(o => o.GetEndTime());
double firstObjectTime = objects.First().StartTime;
return (firstObjectTime, lastObjectTime);
}
#endregion
} }
} }

View File

@ -77,6 +77,8 @@ namespace osu.Game.Rulesets.Edit
private FillFlowContainer togglesCollection; private FillFlowContainer togglesCollection;
private FillFlowContainer sampleBankTogglesCollection;
private IBindable<bool> hasTiming; private IBindable<bool> hasTiming;
private Bindable<bool> autoSeekOnPlacement; private Bindable<bool> autoSeekOnPlacement;
@ -160,6 +162,16 @@ namespace osu.Game.Rulesets.Edit
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5), Spacing = new Vector2(0, 5),
}, },
},
new EditorToolboxGroup("bank (Shift-Q~R)")
{
Child = sampleBankTogglesCollection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
},
} }
} }
}, },
@ -197,6 +209,8 @@ namespace osu.Game.Rulesets.Edit
TernaryStates = CreateTernaryButtons().ToArray(); TernaryStates = CreateTernaryButtons().ToArray();
togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b))); togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b)));
sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Select(b => new DrawableTernaryButton(b)));
setSelectTool(); setSelectTool();
EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged; EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
@ -249,7 +263,7 @@ namespace osu.Game.Rulesets.Edit
/// <summary> /// <summary>
/// Create all ternary states required to be displayed to the user. /// Create all ternary states required to be displayed to the user.
/// </summary> /// </summary>
protected virtual IEnumerable<TernaryButton> CreateTernaryButtons() => BlueprintContainer.TernaryStates; protected virtual IEnumerable<TernaryButton> CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;
/// <summary> /// <summary>
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
@ -274,7 +288,7 @@ namespace osu.Game.Rulesets.Edit
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
{ {
if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed) if (e.ControlPressed || e.AltPressed || e.SuperPressed)
return false; return false;
if (checkLeftToggleFromKey(e.Key, out int leftIndex)) if (checkLeftToggleFromKey(e.Key, out int leftIndex))
@ -291,7 +305,9 @@ namespace osu.Game.Rulesets.Edit
if (checkRightToggleFromKey(e.Key, out int rightIndex)) if (checkRightToggleFromKey(e.Key, out int rightIndex))
{ {
var item = togglesCollection.ElementAtOrDefault(rightIndex); var item = e.ShiftPressed
? sampleBankTogglesCollection.ElementAtOrDefault(rightIndex)
: togglesCollection.ElementAtOrDefault(rightIndex);
if (item is DrawableTernaryButton button) if (item is DrawableTernaryButton button)
{ {

View File

@ -32,6 +32,11 @@ namespace osu.Game.Rulesets.Edit
/// </summary> /// </summary>
public PlacementState PlacementActive { get; private set; } public PlacementState PlacementActive { get; private set; }
/// <summary>
/// Whether the sample bank should be taken from the previous hit object.
/// </summary>
public bool AutomaticBankAssignment { get; set; }
/// <summary> /// <summary>
/// The <see cref="HitObject"/> that is being placed. /// The <see cref="HitObject"/> that is being placed.
/// </summary> /// </summary>
@ -86,11 +91,6 @@ namespace osu.Game.Rulesets.Edit
/// <param name="commitStart">Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments.</param> /// <param name="commitStart">Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments.</param>
protected void BeginPlacement(bool commitStart = false) protected void BeginPlacement(bool commitStart = false)
{ {
// Take the hitnormal sample of the last hit object
var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (lastHitNormal != null)
HitObject.Samples[0] = lastHitNormal;
placementHandler.BeginPlacement(HitObject); placementHandler.BeginPlacement(HitObject);
if (commitStart) if (commitStart)
PlacementActive = PlacementState.Active; PlacementActive = PlacementState.Active;
@ -155,6 +155,14 @@ namespace osu.Game.Rulesets.Edit
if (HitObject is IHasComboInformation comboInformation) if (HitObject is IHasComboInformation comboInformation)
comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation); comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation);
} }
if (AutomaticBankAssignment)
{
// Take the hitnormal sample of the last hit object
var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (lastHitNormal != null)
HitObject.Samples[0] = lastHitNormal;
}
} }
/// <summary> /// <summary>

View File

@ -92,7 +92,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
}; };
SelectionHandler = CreateSelectionHandler(); SelectionHandler = CreateSelectionHandler();
SelectionHandler.DeselectAll = DeselectAll;
SelectionHandler.SelectedItems.BindTo(SelectedItems); SelectionHandler.SelectedItems.BindTo(SelectedItems);
AddRangeInternal(new[] AddRangeInternal(new[]

View File

@ -14,6 +14,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
@ -56,7 +58,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
TernaryStates = CreateTernaryButtons().ToArray(); MainTernaryStates = CreateTernaryButtons().ToArray();
SampleBankTernaryStates = createSampleBankTernaryButtons().ToArray();
AddInternal(new DrawableRulesetDependenciesProvidingContainer(Composer.Ruleset) AddInternal(new DrawableRulesetDependenciesProvidingContainer(Composer.Ruleset)
{ {
@ -78,9 +81,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
// we own SelectionHandler so don't need to worry about making bindable copies (for simplicity) // we own SelectionHandler so don't need to worry about making bindable copies (for simplicity)
foreach (var kvp in SelectionHandler.SelectionSampleStates) foreach (var kvp in SelectionHandler.SelectionSampleStates)
{
kvp.Value.BindValueChanged(_ => updatePlacementSamples()); kvp.Value.BindValueChanged(_ => updatePlacementSamples());
}
foreach (var kvp in SelectionHandler.SelectionBankStates)
kvp.Value.BindValueChanged(_ => updatePlacementSamples());
} }
protected override void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject) protected override void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject)
@ -166,6 +170,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
foreach (var kvp in SelectionHandler.SelectionSampleStates) foreach (var kvp in SelectionHandler.SelectionSampleStates)
sampleChanged(kvp.Key, kvp.Value.Value); sampleChanged(kvp.Key, kvp.Value.Value);
foreach (var kvp in SelectionHandler.SelectionBankStates)
bankChanged(kvp.Key, kvp.Value.Value);
} }
private void sampleChanged(string sampleName, TernaryState state) private void sampleChanged(string sampleName, TernaryState state)
@ -190,12 +197,24 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
} }
private void bankChanged(string bankName, TernaryState state)
{
if (CurrentPlacement == null) return;
if (bankName == EditorSelectionHandler.HIT_BANK_AUTO)
CurrentPlacement.AutomaticBankAssignment = state == TernaryState.True;
else if (state == TernaryState.True)
CurrentPlacement.HitObject.Samples = CurrentPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList();
}
public readonly Bindable<TernaryState> NewCombo = new Bindable<TernaryState> { Description = "New Combo" }; public readonly Bindable<TernaryState> NewCombo = new Bindable<TernaryState> { Description = "New Combo" };
/// <summary> /// <summary>
/// A collection of states which will be displayed to the user in the toolbox. /// A collection of states which will be displayed to the user in the toolbox.
/// </summary> /// </summary>
public TernaryButton[] TernaryStates { get; private set; } public TernaryButton[] MainTernaryStates { get; private set; }
public TernaryButton[] SampleBankTernaryStates { get; private set; }
/// <summary> /// <summary>
/// Create all ternary states required to be displayed to the user. /// Create all ternary states required to be displayed to the user.
@ -209,6 +228,39 @@ namespace osu.Game.Screens.Edit.Compose.Components
yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => getIconForSample(kvp.Key)); yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => getIconForSample(kvp.Key));
} }
private IEnumerable<TernaryButton> createSampleBankTernaryButtons()
{
foreach (var kvp in SelectionHandler.SelectionBankStates)
yield return new TernaryButton(kvp.Value, kvp.Key.Titleize(), () => getIconForBank(kvp.Key));
}
private Drawable getIconForBank(string sampleName)
{
return new Container
{
Size = new Vector2(30, 20),
Children = new Drawable[]
{
new SpriteIcon
{
Size = new Vector2(8),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.VolumeOff
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
X = 10,
Y = -1,
Font = OsuFont.Default.With(weight: FontWeight.Bold, size: 20),
Text = $"{char.ToUpperInvariant(sampleName.First())}"
}
}
};
}
private Drawable getIconForSample(string sampleName) private Drawable getIconForSample(string sampleName)
{ {
switch (sampleName) switch (sampleName)

View File

@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -20,6 +21,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
public partial class EditorSelectionHandler : SelectionHandler<HitObject> public partial class EditorSelectionHandler : SelectionHandler<HitObject>
{ {
/// <summary>
/// A special bank name that is only used in the editor UI.
/// When selected and in placement mode, the bank of the last hit object will always be used.
/// </summary>
public const string HIT_BANK_AUTO = "auto";
[Resolved] [Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; } protected EditorBeatmap EditorBeatmap { get; private set; }
@ -48,11 +55,83 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary> /// </summary>
public readonly Dictionary<string, Bindable<TernaryState>> SelectionSampleStates = new Dictionary<string, Bindable<TernaryState>>(); public readonly Dictionary<string, Bindable<TernaryState>> SelectionSampleStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary>
/// The state of each sample bank type for all selected hitobjects.
/// </summary>
public readonly Dictionary<string, Bindable<TernaryState>> SelectionBankStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary> /// <summary>
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions) /// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
/// </summary> /// </summary>
private void createStateBindables() private void createStateBindables()
{ {
foreach (string bankName in HitSampleInfo.AllBanks.Prepend(HIT_BANK_AUTO))
{
var bindable = new Bindable<TernaryState>
{
Description = bankName.Titleize()
};
bindable.ValueChanged += state =>
{
switch (state.NewValue)
{
case TernaryState.False:
if (SelectedItems.Count == 0)
{
// Ensure that if this is the last selected bank, it should remain selected.
if (SelectionBankStates.Values.All(b => b.Value == TernaryState.False))
bindable.Value = TernaryState.True;
}
else
{
// Auto should never apply when there is a selection made.
// This is also required to stop a bindable feedback loop when a HitObject has zero samples (and LINQ `All` below becomes true).
if (bankName == HIT_BANK_AUTO)
break;
// Never remove a sample bank.
// These are basically radio buttons, not toggles.
if (SelectedItems.All(h => h.Samples.All(s => s.Bank == bankName)))
bindable.Value = TernaryState.True;
}
break;
case TernaryState.True:
if (SelectedItems.Count == 0)
{
// Ensure the user can't stack multiple bank selections when there's no hitobject selection.
// Note that in normal scenarios this is sorted out by the feedback from applying the bank to the selected objects.
foreach (var other in SelectionBankStates.Values)
{
if (other != bindable)
other.Value = TernaryState.False;
}
}
else
{
// Auto should just not apply if there's a selection already made.
// Maybe we could make it a disabled button in the future, but right now the editor buttons don't support disabled state.
if (bankName == HIT_BANK_AUTO)
{
bindable.Value = TernaryState.False;
break;
}
AddSampleBank(bankName);
}
break;
}
};
SelectionBankStates[bankName] = bindable;
}
// start with normal selected.
SelectionBankStates[SampleControlPoint.DEFAULT_BANK].Value = TernaryState.True;
foreach (string sampleName in HitSampleInfo.AllAdditions) foreach (string sampleName in HitSampleInfo.AllAdditions)
{ {
var bindable = new Bindable<TernaryState> var bindable = new Bindable<TernaryState>
@ -104,12 +183,33 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.Any(s => s.Name == sampleName)); bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.Any(s => s.Name == sampleName));
} }
foreach ((string bankName, var bindable) in SelectionBankStates)
{
bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.All(s => s.Bank == bankName));
}
} }
#endregion #endregion
#region Ternary state changes #region Ternary state changes
/// <summary>
/// Adds a sample bank to all selected <see cref="HitObject"/>s.
/// </summary>
/// <param name="bankName">The name of the sample bank.</param>
public void AddSampleBank(string bankName)
{
EditorBeatmap.PerformOnSelection(h =>
{
if (h.Samples.All(s => s.Bank == bankName))
return;
h.Samples = h.Samples.Select(s => s.With(newBank: bankName)).ToList();
EditorBeatmap.Update(h);
});
}
/// <summary> /// <summary>
/// Adds a hit sample to all selected <see cref="HitObject"/>s. /// Adds a hit sample to all selected <see cref="HitObject"/>s.
/// </summary> /// </summary>
@ -174,11 +274,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
yield return new TernaryStateToggleMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } }; yield return new TernaryStateToggleMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } };
} }
yield return new OsuMenuItem("Sound") yield return new OsuMenuItem("Sample")
{ {
Items = SelectionSampleStates.Select(kvp => Items = SelectionSampleStates.Select(kvp =>
new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray() new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
}; };
yield return new OsuMenuItem("Bank")
{
Items = SelectionBankStates.Select(kvp =>
new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
};
} }
#endregion #endregion

View File

@ -22,6 +22,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
public const float BORDER_RADIUS = 3; public const float BORDER_RADIUS = 3;
private const float button_padding = 5;
public Func<float, bool> OnRotation; public Func<float, bool> OnRotation;
public Func<Vector2, Anchor, bool> OnScale; public Func<Vector2, Anchor, bool> OnScale;
public Func<Direction, bool, bool> OnFlip; public Func<Direction, bool, bool> OnFlip;
@ -182,6 +184,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
return base.OnKeyDown(e); return base.OnKeyDown(e);
} }
protected override void Update()
{
base.Update();
ensureButtonsOnScreen();
}
private void recreate() private void recreate()
{ {
if (LoadState < LoadState.Loading) if (LoadState < LoadState.Loading)
@ -234,11 +243,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
}, },
buttons = new FillFlowContainer buttons = new FillFlowContainer
{ {
Y = 20, AutoSizeAxes = Axes.X,
AutoSizeAxes = Axes.Both, Height = 30,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Anchor = Anchor.BottomCentre, Margin = new MarginPadding(button_padding),
Origin = Anchor.Centre
} }
}; };
@ -352,5 +360,39 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (activeOperations++ == 0) if (activeOperations++ == 0)
OperationStarted?.Invoke(); OperationStarted?.Invoke();
} }
private void ensureButtonsOnScreen()
{
buttons.Position = Vector2.Zero;
var thisQuad = ScreenSpaceDrawQuad;
// Shrink the parent quad to give a bit of padding so the buttons don't stick *right* on the border.
// AABBFloat assumes no rotation. one would hope the whole editor is not being rotated.
var parentQuad = Parent.ScreenSpaceDrawQuad.AABBFloat.Shrink(ToLocalSpace(thisQuad.TopLeft + new Vector2(button_padding * 2)));
float topExcess = thisQuad.TopLeft.Y - parentQuad.TopLeft.Y;
float bottomExcess = parentQuad.BottomLeft.Y - thisQuad.BottomLeft.Y;
float leftExcess = thisQuad.TopLeft.X - parentQuad.TopLeft.X;
float rightExcess = parentQuad.TopRight.X - thisQuad.TopRight.X;
if (topExcess + bottomExcess < buttons.Height + button_padding)
{
buttons.Anchor = Anchor.BottomCentre;
buttons.Origin = Anchor.BottomCentre;
}
else if (topExcess > bottomExcess)
{
buttons.Anchor = Anchor.TopCentre;
buttons.Origin = Anchor.BottomCentre;
}
else
{
buttons.Anchor = Anchor.BottomCentre;
buttons.Origin = Anchor.TopCentre;
}
buttons.X += ToLocalSpace(thisQuad.TopLeft - new Vector2(Math.Min(0, leftExcess)) + new Vector2(Math.Min(0, rightExcess))).X;
}
} }
} }

View File

@ -197,9 +197,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Selection Handling #region Selection Handling
/// <summary> /// <summary>
/// Bind an action to deselect all selected blueprints. /// Deselect all selected items.
/// </summary> /// </summary>
internal Action DeselectAll { private get; set; } protected void DeselectAll() => SelectedItems.Clear();
/// <summary> /// <summary>
/// Handle a blueprint becoming selected. /// Handle a blueprint becoming selected.
@ -303,7 +303,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (blueprint.IsSelected) if (blueprint.IsSelected)
return false; return false;
DeselectAll?.Invoke(); DeselectAll();
blueprint.Select(); blueprint.Select();
return true; return true;
} }
@ -311,6 +311,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected void DeleteSelected() protected void DeleteSelected()
{ {
DeleteItems(SelectedItems.ToArray()); DeleteItems(SelectedItems.ToArray());
DeselectAll();
} }
#endregion #endregion

View File

@ -3,39 +3,14 @@
#nullable disable #nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{ {
public partial class MatchTypePill : OnlinePlayComposite public partial class MatchTypePill : OnlinePlayPill
{ {
private OsuTextFlowContainer textFlow;
public MatchTypePill()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new PillContainer
{
Child = textFlow = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
}
};
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -45,8 +20,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
private void onMatchTypeChanged(ValueChangedEvent<MatchType> type) private void onMatchTypeChanged(ValueChangedEvent<MatchType> type)
{ {
textFlow.Clear(); TextFlow.Text = type.NewValue.GetLocalisableDescription();
textFlow.AddText(type.NewValue.GetLocalisableDescription());
} }
} }
} }

View File

@ -0,0 +1,37 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public abstract partial class OnlinePlayPill : OnlinePlayComposite
{
protected PillContainer Pill { get; private set; } = null!;
protected OsuTextFlowContainer TextFlow { get; private set; } = null!;
protected virtual FontUsage Font => OsuFont.GetFont(size: 12);
protected OnlinePlayPill()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = Pill = new PillContainer
{
Child = TextFlow = new OsuTextFlowContainer(s => s.Font = Font)
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
}
};
}
}
}

View File

@ -5,40 +5,16 @@
using System.Linq; using System.Linq;
using Humanizer; using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{ {
/// <summary> /// <summary>
/// A pill that displays the playlist item count. /// A pill that displays the playlist item count.
/// </summary> /// </summary>
public partial class PlaylistCountPill : OnlinePlayComposite public partial class PlaylistCountPill : OnlinePlayPill
{ {
private OsuTextFlowContainer count;
public PlaylistCountPill()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new PillContainer
{
Child = count = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -55,10 +31,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
? Playlist.Count(i => !i.Expired) ? Playlist.Count(i => !i.Expired)
: PlaylistItemStats.Value.CountActive; : PlaylistItemStats.Value.CountActive;
count.Clear(); TextFlow.Clear();
count.AddText(activeItems.ToLocalisableString(), s => s.Font = s.Font.With(weight: FontWeight.Bold)); TextFlow.AddText(activeItems.ToLocalisableString(), s => s.Font = s.Font.With(weight: FontWeight.Bold));
count.AddText(" "); TextFlow.AddText(" ");
count.AddText("Beatmap".ToQuantity(activeItems, ShowQuantityAs.None)); TextFlow.AddText("Beatmap".ToQuantity(activeItems, ShowQuantityAs.None));
} }
} }
} }

View File

@ -3,39 +3,14 @@
#nullable disable #nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{ {
public partial class QueueModePill : OnlinePlayComposite public partial class QueueModePill : OnlinePlayPill
{ {
private OsuTextFlowContainer textFlow;
public QueueModePill()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new PillContainer
{
Child = textFlow = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
}
};
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -45,8 +20,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
private void onQueueModeChanged(ValueChangedEvent<QueueMode> mode) private void onQueueModeChanged(ValueChangedEvent<QueueMode> mode)
{ {
textFlow.Clear(); TextFlow.Text = mode.NewValue.GetLocalisableDescription();
textFlow.AddText(mode.NewValue.GetLocalisableDescription());
} }
} }
} }

View File

@ -5,58 +5,30 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{ {
public partial class RoomSpecialCategoryPill : OnlinePlayComposite public partial class RoomSpecialCategoryPill : OnlinePlayPill
{ {
private SpriteText text;
private PillContainer pill;
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
public RoomSpecialCategoryPill() protected override FontUsage Font => base.Font.With(weight: FontWeight.SemiBold);
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = pill = new PillContainer
{
Background =
{
Colour = colours.Pink,
Alpha = 1
},
Child = text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 12),
Colour = Color4.Black
}
};
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
Pill.Background.Alpha = 1;
TextFlow.Colour = Color4.Black;
Category.BindValueChanged(c => Category.BindValueChanged(c =>
{ {
text.Text = c.NewValue.GetLocalisableDescription(); TextFlow.Text = c.NewValue.GetLocalisableDescription();
Pill.Background.Colour = colours.ForRoomCategory(c.NewValue) ?? colours.Pink;
var backgroundColour = colours.ForRoomCategory(Category.Value);
if (backgroundColour != null)
pill.Background.Colour = backgroundColour.Value;
}, true); }, true);
} }
} }

View File

@ -8,43 +8,20 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Online.Rooms.RoomStatuses;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{ {
/// <summary> /// <summary>
/// A pill that displays the room's current status. /// A pill that displays the room's current status.
/// </summary> /// </summary>
public partial class RoomStatusPill : OnlinePlayComposite public partial class RoomStatusPill : OnlinePlayPill
{ {
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
private PillContainer pill; protected override FontUsage Font => base.Font.With(weight: FontWeight.SemiBold);
private SpriteText statusText;
public RoomStatusPill()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = pill = new PillContainer
{
Child = statusText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 12),
Colour = Color4.Black
}
};
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
@ -54,15 +31,17 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
Status.BindValueChanged(_ => updateDisplay(), true); Status.BindValueChanged(_ => updateDisplay(), true);
FinishTransforms(true); FinishTransforms(true);
TextFlow.Colour = Colour4.Black;
Pill.Background.Alpha = 1;
} }
private void updateDisplay() private void updateDisplay()
{ {
RoomStatus status = getDisplayStatus(); RoomStatus status = getDisplayStatus();
pill.Background.Alpha = 1; Pill.Background.FadeColour(status.GetAppropriateColour(colours), 100);
pill.Background.FadeColour(status.GetAppropriateColour(colours), 100); TextFlow.Text = status.Message;
statusText.Text = status.Message;
} }
private RoomStatus getDisplayStatus() private RoomStatus getDisplayStatus()

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -120,7 +121,7 @@ namespace osu.Game.Screens.Play
State.ValueChanged += _ => InternalButtons.Deselect(); State.ValueChanged += _ => InternalButtons.Deselect();
updateRetryCount(); updateInfoText();
} }
private int retries; private int retries;
@ -135,11 +136,16 @@ namespace osu.Game.Screens.Play
retries = value; retries = value;
if (IsLoaded) if (IsLoaded)
updateRetryCount(); updateInfoText();
} }
} }
protected override void PopIn() => this.FadeIn(TRANSITION_DURATION, Easing.In); protected override void PopIn()
{
this.FadeIn(TRANSITION_DURATION, Easing.In);
updateInfoText();
}
protected override void PopOut() => this.FadeOut(TRANSITION_DURATION, Easing.In); protected override void PopOut() => this.FadeOut(TRANSITION_DURATION, Easing.In);
// Don't let mouse down events through the overlay or people can click circles while paused. // Don't let mouse down events through the overlay or people can click circles while paused.
@ -194,14 +200,39 @@ namespace osu.Game.Screens.Play
{ {
} }
private void updateRetryCount() [Resolved]
{ private IGameplayClock? gameplayClock { get; set; }
// "You've retried 1,065 times in this session"
// "You've retried 1 time in this session"
[Resolved]
private GameplayState? gameplayState { get; set; }
private void updateInfoText()
{
playInfoText.Clear(); playInfoText.Clear();
playInfoText.AddText("Retry count: "); playInfoText.AddText("Retry count: ");
playInfoText.AddText(retries.ToString(), cp => cp.Font = cp.Font.With(weight: FontWeight.Bold)); playInfoText.AddText(retries.ToString(), cp => cp.Font = cp.Font.With(weight: FontWeight.Bold));
if (getSongProgress() is int progress)
{
playInfoText.NewLine();
playInfoText.AddText("Song progress: ");
playInfoText.AddText($"{progress}%", cp => cp.Font = cp.Font.With(weight: FontWeight.Bold));
}
}
private int? getSongProgress()
{
if (gameplayClock == null || gameplayState == null)
return null;
(double firstHitTime, double lastHitTime) = gameplayState.Beatmap.CalculatePlayableBounds();
double playableLength = (lastHitTime - firstHitTime);
if (playableLength == 0)
return 0;
return (int)Math.Clamp(((gameplayClock.CurrentTime - firstHitTime) / playableLength) * 100, 0, 100);
} }
private partial class Button : DialogButton private partial class Button : DialogButton

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -26,8 +27,7 @@ namespace osu.Game.Screens.Play.HUD
if (!objects.Any()) if (!objects.Any())
return; return;
double firstHit = objects.First().StartTime; (double firstHit, double lastHit) = BeatmapExtensions.CalculatePlayableBounds(objects);
double lastHit = objects.Max(o => o.GetEndTime());
if (lastHit == 0) if (lastHit == 0)
lastHit = objects.Last().StartTime; lastHit = objects.Last().StartTime;

View File

@ -6,6 +6,7 @@
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
@ -26,8 +27,7 @@ namespace osu.Game.Screens.Play.HUD
if (!objects.Any()) if (!objects.Any())
return; return;
double firstHit = objects.First().StartTime; (double firstHit, double lastHit) = BeatmapExtensions.CalculatePlayableBounds(objects);
double lastHit = objects.Max(o => o.GetEndTime());
if (lastHit == 0) if (lastHit == 0)
lastHit = objects.Last().StartTime; lastHit = objects.Last().StartTime;

View File

@ -2,12 +2,12 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -52,9 +52,9 @@ namespace osu.Game.Screens.Play.HUD
set set
{ {
objects = value; objects = value;
FirstHitTime = objects.FirstOrDefault()?.StartTime ?? 0;
//TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list). (FirstHitTime, LastHitTime) = BeatmapExtensions.CalculatePlayableBounds(objects);
LastHitTime = objects.LastOrDefault()?.GetEndTime() ?? 0;
UpdateObjects(objects); UpdateObjects(objects);
} }
} }

View File

@ -145,12 +145,12 @@ namespace osu.Game.Screens.Play.HUD
double time = gameplayClock?.CurrentTime ?? Time.Current; double time = gameplayClock?.CurrentTime ?? Time.Current;
double songCurrentTime = time - startTime; double songCurrentTime = time - startTime;
int currentPercent = Math.Max(0, Math.Min(100, (int)(songCurrentTime / songLength * 100))); int currentPercent = songLength == 0 ? 0 : Math.Max(0, Math.Min(100, (int)(songCurrentTime / songLength * 100)));
int currentSecond = (int)Math.Floor(songCurrentTime / 1000.0); int currentSecond = (int)Math.Floor(songCurrentTime / 1000.0);
if (currentPercent != previousPercent) if (currentPercent != previousPercent)
{ {
progress.Text = currentPercent + @"%"; progress.Text = $@"{currentPercent}%";
previousPercent = currentPercent; previousPercent = currentPercent;
} }