Merge branch 'master' into gameplay-hud-redesign/counters
@ -7,7 +7,7 @@ Templates for use when creating osu! dependent projects. Create a fully-testable
|
|||||||
```bash
|
```bash
|
||||||
# install (or update) templates package.
|
# install (or update) templates package.
|
||||||
# this only needs to be done once
|
# this only needs to be done once
|
||||||
dotnet new -i ppy.osu.Game.Templates
|
dotnet new install ppy.osu.Game.Templates
|
||||||
|
|
||||||
# create an empty freeform ruleset
|
# create an empty freeform ruleset
|
||||||
dotnet new ruleset -n MyCoolRuleset
|
dotnet new ruleset -n MyCoolRuleset
|
||||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics;
|
|||||||
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.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Catch.UI;
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
@ -179,5 +180,33 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
updateDistanceSnapGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDistanceSnapGrid()
|
||||||
|
{
|
||||||
|
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True)
|
||||||
|
{
|
||||||
|
distanceSnapGrid.Hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceHitObject = getDistanceSnapGridSourceHitObject();
|
||||||
|
|
||||||
|
if (sourceHitObject == null)
|
||||||
|
{
|
||||||
|
distanceSnapGrid.Hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
distanceSnapGrid.Show();
|
||||||
|
distanceSnapGrid.StartTime = sourceHitObject.GetEndTime();
|
||||||
|
distanceSnapGrid.StartX = sourceHitObject.EffectiveX;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
RelativeSizeAxes = Axes.X
|
RelativeSizeAxes = Axes.X
|
||||||
},
|
},
|
||||||
tailContainer = new Container<DrawableHoldNoteTail> { RelativeSizeAxes = Axes.Both },
|
tailContainer = new Container<DrawableHoldNoteTail> { RelativeSizeAxes = Axes.Both },
|
||||||
slidingSample = new PausableSkinnableSound { Looping = true }
|
slidingSample = new PausableSkinnableSound
|
||||||
|
{
|
||||||
|
Looping = true,
|
||||||
|
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
maskedContents.AddRange(new[]
|
maskedContents.AddRange(new[]
|
||||||
|
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 5.1 KiB |
@ -1,3 +1,4 @@
|
|||||||
[General]
|
[General]
|
||||||
Version: latest
|
Version: latest
|
||||||
HitCircleOverlayAboveNumber: 0
|
HitCircleOverlayAboveNumber: 0
|
||||||
|
HitCirclePrefix: display
|
135
osu.Game.Rulesets.Osu.Tests/SpinnerSpinHistoryTest.cs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// 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.Rulesets.Osu.Objects.Drawables;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class SpinnerSpinHistoryTest
|
||||||
|
{
|
||||||
|
private SpinnerSpinHistory history = null!;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
history = new SpinnerSpinHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(0, 0)]
|
||||||
|
[TestCase(10, 10)]
|
||||||
|
[TestCase(180, 180)]
|
||||||
|
[TestCase(350, 350)]
|
||||||
|
[TestCase(360, 360)]
|
||||||
|
[TestCase(370, 370)]
|
||||||
|
[TestCase(540, 540)]
|
||||||
|
[TestCase(720, 720)]
|
||||||
|
// ---
|
||||||
|
[TestCase(-0, 0)]
|
||||||
|
[TestCase(-10, 10)]
|
||||||
|
[TestCase(-180, 180)]
|
||||||
|
[TestCase(-350, 350)]
|
||||||
|
[TestCase(-360, 360)]
|
||||||
|
[TestCase(-370, 370)]
|
||||||
|
[TestCase(-540, 540)]
|
||||||
|
[TestCase(-720, 720)]
|
||||||
|
public void TestSpinOneDirection(float spin, float expectedRotation)
|
||||||
|
{
|
||||||
|
history.ReportDelta(500, spin);
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(0, 0, 0, 0)]
|
||||||
|
// ---
|
||||||
|
[TestCase(10, -10, 0, 10)]
|
||||||
|
[TestCase(-10, 10, 0, 10)]
|
||||||
|
// ---
|
||||||
|
[TestCase(10, -20, 0, 10)]
|
||||||
|
[TestCase(-10, 20, 0, 10)]
|
||||||
|
// ---
|
||||||
|
[TestCase(20, -10, 0, 20)]
|
||||||
|
[TestCase(-20, 10, 0, 20)]
|
||||||
|
// ---
|
||||||
|
[TestCase(10, -360, 0, 350)]
|
||||||
|
[TestCase(-10, 360, 0, 350)]
|
||||||
|
// ---
|
||||||
|
[TestCase(360, -10, 0, 370)]
|
||||||
|
[TestCase(360, 10, 0, 370)]
|
||||||
|
[TestCase(-360, 10, 0, 370)]
|
||||||
|
[TestCase(-360, -10, 0, 370)]
|
||||||
|
// ---
|
||||||
|
[TestCase(10, 10, 10, 30)]
|
||||||
|
[TestCase(10, 10, -10, 20)]
|
||||||
|
[TestCase(10, -10, 10, 10)]
|
||||||
|
[TestCase(-10, -10, -10, 30)]
|
||||||
|
[TestCase(-10, -10, 10, 20)]
|
||||||
|
[TestCase(-10, 10, 10, 10)]
|
||||||
|
// ---
|
||||||
|
[TestCase(10, -20, -350, 360)]
|
||||||
|
[TestCase(10, -20, 350, 340)]
|
||||||
|
[TestCase(-10, 20, 350, 360)]
|
||||||
|
[TestCase(-10, 20, -350, 340)]
|
||||||
|
public void TestSpinMultipleDirections(float spin1, float spin2, float spin3, float expectedRotation)
|
||||||
|
{
|
||||||
|
history.ReportDelta(500, spin1);
|
||||||
|
history.ReportDelta(1000, spin2);
|
||||||
|
history.ReportDelta(1500, spin3);
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation));
|
||||||
|
}
|
||||||
|
|
||||||
|
// One spin
|
||||||
|
[TestCase(370, -50, 320)]
|
||||||
|
[TestCase(-370, 50, 320)]
|
||||||
|
// Two spins
|
||||||
|
[TestCase(740, -420, 320)]
|
||||||
|
[TestCase(-740, 420, 320)]
|
||||||
|
public void TestRemoveAndCrossFullSpin(float deltaToAdd, float deltaToRemove, float expectedRotation)
|
||||||
|
{
|
||||||
|
history.ReportDelta(1000, deltaToAdd);
|
||||||
|
history.ReportDelta(500, deltaToRemove);
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation));
|
||||||
|
}
|
||||||
|
|
||||||
|
// One spin + partial
|
||||||
|
[TestCase(400, -30, -50, 320)]
|
||||||
|
[TestCase(-400, 30, 50, 320)]
|
||||||
|
// Two spins + partial
|
||||||
|
[TestCase(800, -430, -50, 320)]
|
||||||
|
[TestCase(-800, 430, 50, 320)]
|
||||||
|
public void TestRemoveAndCrossFullAndPartialSpins(float deltaToAdd1, float deltaToAdd2, float deltaToRemove, float expectedRotation)
|
||||||
|
{
|
||||||
|
history.ReportDelta(1000, deltaToAdd1);
|
||||||
|
history.ReportDelta(1500, deltaToAdd2);
|
||||||
|
history.ReportDelta(500, deltaToRemove);
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRewindMultipleFullSpins()
|
||||||
|
{
|
||||||
|
history.ReportDelta(500, 360);
|
||||||
|
history.ReportDelta(1000, 720);
|
||||||
|
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(1080));
|
||||||
|
|
||||||
|
history.ReportDelta(250, -900);
|
||||||
|
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(180));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRewindOverDirectionChange()
|
||||||
|
{
|
||||||
|
history.ReportDelta(1000, 40); // max is now CW 40 degrees
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(40));
|
||||||
|
history.ReportDelta(1100, -90); // max is now CCW 50 degrees
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(50));
|
||||||
|
history.ReportDelta(1200, 110); // max is now CW 60 degrees
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(60));
|
||||||
|
|
||||||
|
history.ReportDelta(1000, -20);
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(40));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Size = new Vector2(0.8f),
|
Size = new Vector2(0.8f),
|
||||||
Child = new MovingCursorInputManager { Child = createContent?.Invoke() }
|
Child = new MovingCursorInputManager { Child = createContent() }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true)));
|
AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true)));
|
||||||
AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true)));
|
AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true)));
|
||||||
AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true)));
|
AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true)));
|
||||||
|
AddStep("High combo index", () => SetContents(_ => testSingle(2, true, comboIndex: 15)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -66,12 +67,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
|
AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
|
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null, int comboIndex = 0)
|
||||||
{
|
{
|
||||||
var playfield = new TestOsuPlayfield();
|
var playfield = new TestOsuPlayfield();
|
||||||
|
|
||||||
for (double t = timeOffset; t < timeOffset + 60000; t += 2000)
|
for (double t = timeOffset; t < timeOffset + 60000; t += 2000)
|
||||||
playfield.Add(createSingle(circleSize, auto, t, positionOffset));
|
playfield.Add(createSingle(circleSize, auto, t, positionOffset, comboIndex: comboIndex));
|
||||||
|
|
||||||
return playfield;
|
return playfield;
|
||||||
}
|
}
|
||||||
@ -84,14 +85,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
for (int i = 0; i <= 1000; i += 100)
|
for (int i = 0; i <= 1000; i += 100)
|
||||||
{
|
{
|
||||||
playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset));
|
playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset, i / 100 - 1));
|
||||||
pos.X += 50;
|
pos.X += 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
return playfield;
|
return playfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0)
|
private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0, int comboIndex = 0)
|
||||||
{
|
{
|
||||||
positionOffset ??= Vector2.Zero;
|
positionOffset ??= Vector2.Zero;
|
||||||
|
|
||||||
@ -99,6 +100,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
StartTime = Time.Current + 1000 + timeOffset,
|
StartTime = Time.Current + 1000 + timeOffset,
|
||||||
Position = OsuPlayfield.BASE_SIZE / 4 + positionOffset.Value,
|
Position = OsuPlayfield.BASE_SIZE / 4 + positionOffset.Value,
|
||||||
|
IndexInCurrentCombo = comboIndex,
|
||||||
};
|
};
|
||||||
|
|
||||||
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
|
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
|
||||||
|
@ -38,6 +38,42 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
private readonly List<JudgementResult> judgementResults = new List<JudgementResult>();
|
private readonly List<JudgementResult> judgementResults = new List<JudgementResult>();
|
||||||
|
|
||||||
|
[TestCase(30, 0)]
|
||||||
|
[TestCase(30, 1)]
|
||||||
|
[TestCase(40, 0)]
|
||||||
|
[TestCase(40, 1)]
|
||||||
|
[TestCase(50, 1)]
|
||||||
|
[TestCase(60, 1)]
|
||||||
|
[TestCase(70, 1)]
|
||||||
|
[TestCase(80, 1)]
|
||||||
|
[TestCase(80, 0)]
|
||||||
|
[TestCase(80, 10)]
|
||||||
|
[TestCase(90, 1)]
|
||||||
|
[Ignore("headless test doesn't run at high enough precision for this to always enter a tracking state in time.")]
|
||||||
|
public void TestVeryShortSliderMissHead(float sliderLength, int repeatCount)
|
||||||
|
{
|
||||||
|
performTest(new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Position = new Vector2(50, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start - 10 },
|
||||||
|
new OsuReplayFrame { Position = new Vector2(50, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 2000 },
|
||||||
|
}, new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_slider_start,
|
||||||
|
Position = new Vector2(0, 0),
|
||||||
|
SliderVelocityMultiplier = 10f,
|
||||||
|
RepeatCount = repeatCount,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(sliderLength, 0),
|
||||||
|
}),
|
||||||
|
}, 240, 1);
|
||||||
|
|
||||||
|
AddAssert("Head judgement is first", () => judgementResults[0].HitObject is SliderHeadCircle);
|
||||||
|
AddAssert("Tail judgement is second last", () => judgementResults[^2].HitObject is SliderTailCircle);
|
||||||
|
AddAssert("Slider judgement is last", () => judgementResults[^1].HitObject is Slider);
|
||||||
|
}
|
||||||
|
|
||||||
// Making these too short causes breakage from frames not being processed fast enough.
|
// Making these too short causes breakage from frames not being processed fast enough.
|
||||||
// To keep things simple, these tests are crafted to always be >16ms length.
|
// To keep things simple, these tests are crafted to always be >16ms length.
|
||||||
// If sliders shorter than this are ever used in gameplay it will probably break things and we can revisit.
|
// If sliders shorter than this are ever used in gameplay it will probably break things and we can revisit.
|
||||||
@ -76,6 +112,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
assertAllMaxJudgements();
|
assertAllMaxJudgements();
|
||||||
|
|
||||||
|
AddAssert("Head judgement is first", () => judgementResults.First().HitObject is SliderHeadCircle);
|
||||||
|
|
||||||
// Even if the last tick is hit early, the slider should always execute its final judgement at its endtime.
|
// Even if the last tick is hit early, the slider should always execute its final judgement at its endtime.
|
||||||
// If not, hitsounds will not play on time.
|
// If not, hitsounds will not play on time.
|
||||||
AddAssert("Judgement offset is zero", () => judgementResults.Last().TimeOffset == 0);
|
AddAssert("Judgement offset is zero", () => judgementResults.Last().TimeOffset == 0);
|
||||||
@ -121,6 +159,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
else
|
else
|
||||||
AddAssert("Tracking dropped", assertMidSliderJudgementFail);
|
AddAssert("Tracking dropped", assertMidSliderJudgementFail);
|
||||||
|
|
||||||
|
AddAssert("Head judgement is first", () => judgementResults.First().HitObject is SliderHeadCircle);
|
||||||
|
|
||||||
// Even if the last tick is hit early, the slider should always execute its final judgement at its endtime.
|
// Even if the last tick is hit early, the slider should always execute its final judgement at its endtime.
|
||||||
// If not, hitsounds will not play on time.
|
// If not, hitsounds will not play on time.
|
||||||
AddAssert("Judgement offset is zero", () => judgementResults.Last().TimeOffset == 0);
|
AddAssert("Judgement offset is zero", () => judgementResults.Last().TimeOffset == 0);
|
||||||
|
@ -53,7 +53,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
/// While off-centre, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms.
|
/// While off-centre, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
[Ignore("An upcoming implementation will fix this case")]
|
|
||||||
public void TestVibrateWithoutSpinningOffCentre()
|
public void TestVibrateWithoutSpinningOffCentre()
|
||||||
{
|
{
|
||||||
List<ReplayFrame> frames = new List<ReplayFrame>();
|
List<ReplayFrame> frames = new List<ReplayFrame>();
|
||||||
@ -81,7 +80,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
/// While centred on the slider, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms.
|
/// While centred on the slider, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
[Ignore("An upcoming implementation will fix this case")]
|
|
||||||
public void TestVibrateWithoutSpinningOnCentre()
|
public void TestVibrateWithoutSpinningOnCentre()
|
||||||
{
|
{
|
||||||
List<ReplayFrame> frames = new List<ReplayFrame>();
|
List<ReplayFrame> frames = new List<ReplayFrame>();
|
||||||
@ -130,7 +128,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
/// No ticks should be hit since the total rotation is -0.5 (0.5 CW + 1 CCW = 0.5 CCW).
|
/// No ticks should be hit since the total rotation is -0.5 (0.5 CW + 1 CCW = 0.5 CCW).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
[Ignore("An upcoming implementation will fix this case")]
|
|
||||||
public void TestSpinHalfBothDirections()
|
public void TestSpinHalfBothDirections()
|
||||||
{
|
{
|
||||||
performTest(new SpinFramesGenerator(time_spinner_start)
|
performTest(new SpinFramesGenerator(time_spinner_start)
|
||||||
@ -149,7 +146,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
[TestCase(-180, 540, 1)]
|
[TestCase(-180, 540, 1)]
|
||||||
[TestCase(180, -900, 2)]
|
[TestCase(180, -900, 2)]
|
||||||
[TestCase(-180, 900, 2)]
|
[TestCase(-180, 900, 2)]
|
||||||
[Ignore("An upcoming implementation will fix this case")]
|
|
||||||
public void TestSpinOneDirectionThenChangeDirection(float direction1, float direction2, int expectedTicks)
|
public void TestSpinOneDirectionThenChangeDirection(float direction1, float direction2, int expectedTicks)
|
||||||
{
|
{
|
||||||
performTest(new SpinFramesGenerator(time_spinner_start)
|
performTest(new SpinFramesGenerator(time_spinner_start)
|
||||||
@ -162,18 +158,28 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[Ignore("An upcoming implementation will fix this case")]
|
|
||||||
public void TestRewind()
|
public void TestRewind()
|
||||||
{
|
{
|
||||||
AddStep("set manual clock", () => manualClock = new ManualClock { Rate = 1 });
|
AddStep("set manual clock", () => manualClock = new ManualClock
|
||||||
|
{
|
||||||
|
// Avoids interpolation trying to run ahead during testing.
|
||||||
|
Rate = 0
|
||||||
|
});
|
||||||
|
|
||||||
List<ReplayFrame> frames = new SpinFramesGenerator(time_spinner_start)
|
List<ReplayFrame> frames =
|
||||||
.Spin(360, 500) // 2000ms -> 1 full CW spin
|
new SpinFramesGenerator(time_spinner_start)
|
||||||
.Spin(-180, 500) // 2500ms -> 0.5 CCW spins
|
// 1500ms start
|
||||||
.Spin(90, 500) // 3000ms -> 0.25 CW spins
|
.Spin(360, 500)
|
||||||
.Spin(450, 500) // 3500ms -> 1 full CW spin
|
// 2000ms -> 1 full CW spin
|
||||||
.Spin(180, 500) // 4000ms -> 0.5 CW spins
|
.Spin(-180, 500)
|
||||||
.Build();
|
// 2500ms -> 1 full CW spin + 0.5 CCW spins
|
||||||
|
.Spin(90, 500)
|
||||||
|
// 3000ms -> 1 full CW spin + 0.25 CCW spins
|
||||||
|
.Spin(450, 500)
|
||||||
|
// 3500ms -> 2 full CW spins
|
||||||
|
.Spin(180, 500)
|
||||||
|
// 4000ms -> 2 full CW spins + 0.5 CW spins
|
||||||
|
.Build();
|
||||||
|
|
||||||
loadPlayer(frames);
|
loadPlayer(frames);
|
||||||
|
|
||||||
@ -190,15 +196,35 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
DrawableSpinner drawableSpinner = null!;
|
DrawableSpinner drawableSpinner = null!;
|
||||||
AddUntilStep("get spinner", () => (drawableSpinner = currentPlayer.ChildrenOfType<DrawableSpinner>().Single()) != null);
|
AddUntilStep("get spinner", () => (drawableSpinner = currentPlayer.ChildrenOfType<DrawableSpinner>().Single()) != null);
|
||||||
|
|
||||||
assertTotalRotation(4000, 900);
|
assertFinalRotationCorrect();
|
||||||
assertTotalRotation(3750, 810);
|
assertTotalRotation(3750, 810);
|
||||||
assertTotalRotation(3500, 720);
|
assertTotalRotation(3500, 720);
|
||||||
assertTotalRotation(3250, 530);
|
assertTotalRotation(3250, 530);
|
||||||
assertTotalRotation(3000, 540);
|
assertTotalRotation(3000, 450);
|
||||||
assertTotalRotation(2750, 540);
|
assertTotalRotation(2750, 540);
|
||||||
assertTotalRotation(2500, 540);
|
assertTotalRotation(2500, 540);
|
||||||
assertTotalRotation(2250, 360);
|
assertTotalRotation(2250, 450);
|
||||||
assertTotalRotation(2000, 180);
|
assertTotalRotation(2000, 360);
|
||||||
|
assertTotalRotation(1500, 0);
|
||||||
|
|
||||||
|
// same thing but always returning to final time to check.
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(3750, 810);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(3500, 720);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(3250, 530);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(3000, 450);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(2750, 540);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(2500, 540);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(2250, 450);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(2000, 360);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
assertTotalRotation(1500, 0);
|
assertTotalRotation(1500, 0);
|
||||||
|
|
||||||
void assertTotalRotation(double time, float expected)
|
void assertTotalRotation(double time, float expected)
|
||||||
@ -211,8 +237,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
void addSeekStep(double time)
|
void addSeekStep(double time)
|
||||||
{
|
{
|
||||||
AddStep($"seek to {time}", () => clock.Seek(time));
|
AddStep($"seek to {time}", () => clock.Seek(time));
|
||||||
|
// Lenience is required due to interpolation running slightly ahead on a stalled clock.
|
||||||
AddUntilStep("wait for seek to finish", () => drawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time));
|
AddUntilStep("wait for seek to finish", () => drawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void assertFinalRotationCorrect() => assertTotalRotation(4000, 900);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertTicksHit(int count)
|
private void assertTicksHit(int count)
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Judgements
|
namespace osu.Game.Rulesets.Osu.Judgements
|
||||||
{
|
{
|
||||||
@ -15,28 +16,15 @@ namespace osu.Game.Rulesets.Osu.Judgements
|
|||||||
public Spinner Spinner => (Spinner)HitObject;
|
public Spinner Spinner => (Spinner)HitObject;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The total rotation performed on the spinner disc, disregarding the spin direction,
|
/// The total amount that the spinner was rotated.
|
||||||
/// adjusted for the track's playback rate.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
public float TotalRotation => History.TotalRotation;
|
||||||
/// <para>
|
|
||||||
/// This value is always non-negative and is monotonically increasing with time
|
/// <summary>
|
||||||
/// (i.e. will only increase if time is passing forward, but can decrease during rewind).
|
/// Stores the spinning history of the spinner.<br />
|
||||||
/// </para>
|
/// Instants of movement deltas may be added or removed from this in order to calculate the total rotation for the spinner.
|
||||||
/// <para>
|
/// </summary>
|
||||||
/// The rotation from each frame is multiplied by the clock's current playback rate.
|
public readonly SpinnerSpinHistory History = new SpinnerSpinHistory();
|
||||||
/// The reason this is done is to ensure that spinners give the same score and require the same number of spins
|
|
||||||
/// regardless of whether speed-modifying mods are applied.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
/// <example>
|
|
||||||
/// Assuming no speed-modifying mods are active,
|
|
||||||
/// if the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
|
|
||||||
/// this property will return the value of 720 (as opposed to 0).
|
|
||||||
/// If Double Time is active instead (with a speed multiplier of 1.5x),
|
|
||||||
/// in the same scenario the property will return 720 * 1.5 = 1080.
|
|
||||||
/// </example>
|
|
||||||
public float TotalRotation;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Time instant at which the spin was started (the first user input which caused an increase in spin).
|
/// Time instant at which the spin was started (the first user input which caused an increase in spin).
|
||||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override LocalisableString Description => "Burn the notes into your memory.";
|
public override LocalisableString Description => "Burn the notes into your memory.";
|
||||||
|
|
||||||
//Alters the transforms of the approach circles, breaking the effects of these mods.
|
//Alters the transforms of the approach circles, breaking the effects of these mods.
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModApproachDifferent) };
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray();
|
||||||
|
|
||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// 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;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override LocalisableString Description => "Everything rotates. EVERYTHING.";
|
public override LocalisableString Description => "Everything rotates. EVERYTHING.";
|
||||||
public override double ScoreMultiplier => 1;
|
public override double ScoreMultiplier => 1;
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) };
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame) }).ToArray();
|
||||||
|
|
||||||
private float theta;
|
private float theta;
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
HeadCircle,
|
HeadCircle,
|
||||||
TailCircle,
|
TailCircle,
|
||||||
|
repeatContainer,
|
||||||
Body,
|
Body,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -107,7 +108,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
|
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
|
||||||
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
|
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
|
||||||
Ball,
|
Ball,
|
||||||
slidingSample = new PausableSkinnableSound { Looping = true }
|
slidingSample = new PausableSkinnableSound
|
||||||
|
{
|
||||||
|
Looping = true,
|
||||||
|
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -15,9 +14,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
public new SliderHeadCircle HitObject => (SliderHeadCircle)base.HitObject;
|
public new SliderHeadCircle HitObject => (SliderHeadCircle)base.HitObject;
|
||||||
|
|
||||||
[CanBeNull]
|
|
||||||
public Slider Slider => DrawableSlider?.HitObject;
|
|
||||||
|
|
||||||
public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
||||||
|
|
||||||
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
|
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
|
||||||
|
@ -17,7 +17,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
{
|
{
|
||||||
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking
|
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking, IRequireTracking
|
||||||
{
|
{
|
||||||
public new SliderRepeat HitObject => (SliderRepeat)base.HitObject;
|
public new SliderRepeat HitObject => (SliderRepeat)base.HitObject;
|
||||||
|
|
||||||
@ -36,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
public override bool DisplayResult => false;
|
public override bool DisplayResult => false;
|
||||||
|
|
||||||
|
public bool Tracking { get; set; }
|
||||||
|
|
||||||
public DrawableSliderRepeat()
|
public DrawableSliderRepeat()
|
||||||
: base(null)
|
: base(null)
|
||||||
{
|
{
|
||||||
@ -85,8 +87,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
if (HitObject.StartTime <= Time.Current)
|
// shared implementation with DrawableSliderTick.
|
||||||
ApplyResult(r => r.Type = DrawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
if (timeOffset >= 0)
|
||||||
|
{
|
||||||
|
// Attempt to preserve correct ordering of judgements as best we can by forcing
|
||||||
|
// an un-judged head to be missed when the user has clearly skipped it.
|
||||||
|
//
|
||||||
|
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
|
||||||
|
if (Tracking && !DrawableSlider.HeadCircle.Judged)
|
||||||
|
DrawableSlider.HeadCircle.MissForcefully();
|
||||||
|
|
||||||
|
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateInitialTransforms()
|
protected override void UpdateInitialTransforms()
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -129,16 +130,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
if (userTriggered)
|
if (userTriggered)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Ensure the tail can only activate after all previous ticks already have.
|
// Ensure the tail can only activate after all previous ticks/repeats already have.
|
||||||
//
|
//
|
||||||
// This covers the edge case where the lenience may allow the tail to activate before
|
// This covers the edge case where the lenience may allow the tail to activate before
|
||||||
// the last tick, changing ordering of score/combo awarding.
|
// the last tick, changing ordering of score/combo awarding.
|
||||||
if (DrawableSlider.NestedHitObjects.Count > 1 && !DrawableSlider.NestedHitObjects[^2].Judged)
|
var lastTick = DrawableSlider.NestedHitObjects.LastOrDefault(o => o.HitObject is SliderTick || o.HitObject is SliderRepeat);
|
||||||
|
if (lastTick?.Judged == false)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (timeOffset < SliderEventGenerator.TAIL_LENIENCY)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Attempt to preserve correct ordering of judgements as best we can by forcing
|
||||||
|
// an un-judged head to be missed when the user has clearly skipped it.
|
||||||
|
//
|
||||||
|
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
|
||||||
|
if (Tracking && !DrawableSlider.HeadCircle.Judged)
|
||||||
|
DrawableSlider.HeadCircle.MissForcefully();
|
||||||
|
|
||||||
// The player needs to have engaged in tracking at any point after the tail leniency cutoff.
|
// The player needs to have engaged in tracking at any point after the tail leniency cutoff.
|
||||||
// An actual tick miss should only occur if reaching the tick itself.
|
// An actual tick miss should only occur if reaching the tick itself.
|
||||||
if (timeOffset >= SliderEventGenerator.TAIL_LENIENCY && Tracking)
|
if (Tracking)
|
||||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||||
else if (timeOffset > 0)
|
else if (timeOffset > 0)
|
||||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||||
|
@ -75,8 +75,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
|
// shared implementation with DrawableSliderRepeat.
|
||||||
if (timeOffset >= 0)
|
if (timeOffset >= 0)
|
||||||
|
{
|
||||||
|
// Attempt to preserve correct ordering of judgements as best we can by forcing
|
||||||
|
// an un-judged head to be missed when the user has clearly skipped it.
|
||||||
|
//
|
||||||
|
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
|
||||||
|
if (Tracking && !DrawableSlider.HeadCircle.Judged)
|
||||||
|
DrawableSlider.HeadCircle.MissForcefully();
|
||||||
|
|
||||||
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateInitialTransforms()
|
protected override void UpdateInitialTransforms()
|
||||||
|
@ -106,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
spinningSample = new PausableSkinnableSound
|
spinningSample = new PausableSkinnableSound
|
||||||
{
|
{
|
||||||
Volume = { Value = 0 },
|
Volume = { Value = 0 },
|
||||||
|
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
|
||||||
Looping = true,
|
Looping = true,
|
||||||
Frequency = { Value = spinning_sample_initial_frequency }
|
Frequency = { Value = spinning_sample_initial_frequency }
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnApply()
|
||||||
|
{
|
||||||
|
base.OnApply();
|
||||||
|
|
||||||
|
// the tick can be theoretically judged at any point in the spinner's duration,
|
||||||
|
// so it must be alive throughout the spinner's entire lifetime.
|
||||||
|
// this mostly matters for correct sample playback.
|
||||||
|
LifetimeStart = DrawableSpinner.HitObject.StartTime;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Apply a judgement result.
|
/// Apply a judgement result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
146
osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerSpinHistory.cs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the spinning history of a single spinner.<br />
|
||||||
|
/// Instants of movement deltas may be added or removed from this in order to calculate the total rotation for the spinner.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A single, full rotation of the spinner is defined as a 360-degree rotation of the spinner, starting from 0, going in a single direction.<br />
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// If the player spins 90-degrees clockwise, then changes direction, they need to spin 90-degrees counter-clockwise to return to 0
|
||||||
|
/// and then continue rotating the spinner for another 360-degrees in the same direction.
|
||||||
|
/// </example>
|
||||||
|
public class SpinnerSpinHistory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The sum of all complete spins and any current partial spin, in degrees.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is the final scoring value.
|
||||||
|
/// </remarks>
|
||||||
|
public float TotalRotation => 360 * completedSpins.Count + currentSpinMaxRotation;
|
||||||
|
|
||||||
|
private readonly Stack<CompletedSpin> completedSpins = new Stack<CompletedSpin>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The total accumulated (absolute) rotation.
|
||||||
|
/// </summary>
|
||||||
|
private float totalAccumulatedRotation;
|
||||||
|
|
||||||
|
private float totalAccumulatedRotationAtLastCompletion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For the current spin, represents the maximum absolute rotation (from 0..360) achieved by the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is used to report <see cref="TotalRotation"/> in the case a user spins backwards.
|
||||||
|
/// Basically it allows us to not reduce the total rotation in such a case.
|
||||||
|
///
|
||||||
|
/// This also stops spinner "cheese" where a user may rapidly change directions and cause an increase
|
||||||
|
/// in rotations.
|
||||||
|
/// </remarks>
|
||||||
|
private float currentSpinMaxRotation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current spin, from -360..360.
|
||||||
|
/// </summary>
|
||||||
|
private float currentSpinRotation => totalAccumulatedRotation - totalAccumulatedRotationAtLastCompletion;
|
||||||
|
|
||||||
|
private double lastReportTime = double.NegativeInfinity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Report a delta update based on user input.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="currentTime">The current time.</param>
|
||||||
|
/// <param name="delta">The delta of the angle moved through since the last report.</param>
|
||||||
|
public void ReportDelta(double currentTime, float delta)
|
||||||
|
{
|
||||||
|
if (delta == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Importantly, outside of tests the max delta entering here is 180 degrees.
|
||||||
|
// If it wasn't for tests, we could add this line:
|
||||||
|
//
|
||||||
|
// Debug.Assert(Math.Abs(delta) < 180);
|
||||||
|
//
|
||||||
|
// For this to be 101% correct, we need to add the ability for important frames to be
|
||||||
|
// created based on gameplay intrinsics (ie. there should be one frame for any spinner delta 90 < n < 180 degrees).
|
||||||
|
//
|
||||||
|
// But this can come later.
|
||||||
|
|
||||||
|
totalAccumulatedRotation += delta;
|
||||||
|
|
||||||
|
if (currentTime >= lastReportTime)
|
||||||
|
{
|
||||||
|
currentSpinMaxRotation = Math.Max(currentSpinMaxRotation, Math.Abs(currentSpinRotation));
|
||||||
|
|
||||||
|
// Handle the case where the user has completed another spin.
|
||||||
|
// Note that this does could be an `if` rather than `while` if the above assertion held true.
|
||||||
|
// It is a `while` loop to handle tests which throw larger values at this method.
|
||||||
|
while (currentSpinMaxRotation >= 360)
|
||||||
|
{
|
||||||
|
int direction = Math.Sign(currentSpinRotation);
|
||||||
|
|
||||||
|
completedSpins.Push(new CompletedSpin(currentTime, direction));
|
||||||
|
|
||||||
|
// Incrementing the last completion point will cause `currentSpinRotation` to
|
||||||
|
// hold the remaining spin that needs to be considered.
|
||||||
|
totalAccumulatedRotationAtLastCompletion += direction * 360;
|
||||||
|
|
||||||
|
// Reset the current max as we are entering a new spin.
|
||||||
|
// Importantly, carry over the remainder (which is now stored in `currentSpinRotation`).
|
||||||
|
currentSpinMaxRotation = Math.Abs(currentSpinRotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// When rewinding, the main thing we care about is getting `totalAbsoluteRotationsAtLastCompletion`
|
||||||
|
// to the correct value. We can used the stored history for this.
|
||||||
|
while (completedSpins.TryPeek(out var segment) && segment.CompletionTime > currentTime)
|
||||||
|
{
|
||||||
|
completedSpins.Pop();
|
||||||
|
totalAccumulatedRotationAtLastCompletion -= segment.Direction * 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a best effort. We may not have enough data to match this 1:1, but that's okay.
|
||||||
|
// We know that the player is somewhere in a spin.
|
||||||
|
// In the worst case, this will be lower than expected, and recover in forward playback.
|
||||||
|
currentSpinMaxRotation = Math.Abs(currentSpinRotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastReportTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single completed spin.
|
||||||
|
/// </summary>
|
||||||
|
private class CompletedSpin
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The time at which this spin completion occurred.
|
||||||
|
/// </summary>
|
||||||
|
public readonly double CompletionTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The direction this spin completed in.
|
||||||
|
/// </summary>
|
||||||
|
public readonly int Direction;
|
||||||
|
|
||||||
|
public CompletedSpin(double completionTime, int direction)
|
||||||
|
{
|
||||||
|
Debug.Assert(direction == -1 || direction == 1);
|
||||||
|
|
||||||
|
CompletionTime = completionTime;
|
||||||
|
Direction = direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -101,15 +101,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
rotationTransferred = true;
|
rotationTransferred = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentRotation += delta;
|
|
||||||
|
|
||||||
double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate;
|
double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate;
|
||||||
|
delta = (float)(delta * Math.Abs(rate));
|
||||||
|
|
||||||
Debug.Assert(Math.Abs(delta) <= 180);
|
Debug.Assert(Math.Abs(delta) <= 180);
|
||||||
|
|
||||||
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
|
currentRotation += delta;
|
||||||
// (see: ModTimeRamp)
|
drawableSpinner.Result.History.ReportDelta(Time.Current, delta);
|
||||||
drawableSpinner.Result.TotalRotation += (float)(Math.Abs(delta) * rate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetState(DrawableHitObject obj)
|
private void resetState(DrawableHitObject obj)
|
||||||
|
@ -1,35 +1,39 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
|
using System;
|
||||||
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.Animations;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||||
{
|
{
|
||||||
public partial class LegacySliderBall : CompositeDrawable
|
public partial class LegacySliderBall : CompositeDrawable
|
||||||
{
|
{
|
||||||
private readonly Drawable animationContent;
|
|
||||||
|
|
||||||
private readonly ISkin skin;
|
private readonly ISkin skin;
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private DrawableHitObject? parentObject { get; set; }
|
private DrawableHitObject? parentObject { get; set; }
|
||||||
|
|
||||||
public Color4 BallColour => animationContent.Colour;
|
|
||||||
|
|
||||||
private Sprite layerNd = null!;
|
private Sprite layerNd = null!;
|
||||||
private Sprite layerSpec = null!;
|
private Sprite layerSpec = null!;
|
||||||
|
|
||||||
public LegacySliderBall(Drawable animationContent, ISkin skin)
|
private TextureAnimation ballAnimation = null!;
|
||||||
|
private Texture[] ballTextures = null!;
|
||||||
|
|
||||||
|
public Color4 BallColour => ballAnimation.Colour;
|
||||||
|
|
||||||
|
public LegacySliderBall(ISkin skin)
|
||||||
{
|
{
|
||||||
this.animationContent = animationContent;
|
|
||||||
this.skin = skin;
|
this.skin = skin;
|
||||||
|
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
@ -38,30 +42,39 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
var ballColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBall)?.Value ?? Color4.White;
|
Vector2 maxSize = OsuLegacySkinTransformer.MAX_FOLLOW_CIRCLE_AREA_SIZE;
|
||||||
|
|
||||||
InternalChildren = new[]
|
var ballColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBall)?.Value ?? Color4.White;
|
||||||
|
ballTextures = skin.GetTextures("sliderb", default, default, true, "", maxSize, out _);
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
layerNd = new Sprite
|
layerNd = new Sprite
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Texture = skin.GetTexture("sliderb-nd")?.WithMaximumSize(OsuLegacySkinTransformer.MAX_FOLLOW_CIRCLE_AREA_SIZE),
|
Texture = skin.GetTexture("sliderb-nd")?.WithMaximumSize(maxSize),
|
||||||
Colour = new Color4(5, 5, 5, 255),
|
Colour = new Color4(5, 5, 5, 255),
|
||||||
},
|
},
|
||||||
LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d =>
|
ballAnimation = new LegacySkinExtensions.SkinnableTextureAnimation
|
||||||
{
|
{
|
||||||
d.Anchor = Anchor.Centre;
|
Anchor = Anchor.Centre,
|
||||||
d.Origin = Anchor.Centre;
|
Origin = Anchor.Centre,
|
||||||
}), ballColour),
|
Colour = ballColour,
|
||||||
|
},
|
||||||
layerSpec = new Sprite
|
layerSpec = new Sprite
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Texture = skin.GetTexture("sliderb-spec")?.WithMaximumSize(OsuLegacySkinTransformer.MAX_FOLLOW_CIRCLE_AREA_SIZE),
|
Texture = skin.GetTexture("sliderb-spec")?.WithMaximumSize(maxSize),
|
||||||
Blending = BlendingParameters.Additive,
|
Blending = BlendingParameters.Additive,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (parentObject != null)
|
||||||
|
parentObject.HitObjectApplied += onHitObjectApplied;
|
||||||
|
|
||||||
|
onHitObjectApplied(parentObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
|
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
|
||||||
@ -78,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
if (skin.GetConfig<SkinConfiguration.LegacySetting, bool>(SkinConfiguration.LegacySetting.AllowSliderBallTint)?.Value == true)
|
if (skin.GetConfig<SkinConfiguration.LegacySetting, bool>(SkinConfiguration.LegacySetting.AllowSliderBallTint)?.Value == true)
|
||||||
{
|
{
|
||||||
accentColour.BindTo(parentObject.AccentColour);
|
accentColour.BindTo(parentObject.AccentColour);
|
||||||
accentColour.BindValueChanged(a => animationContent.Colour = a.NewValue, true);
|
accentColour.BindValueChanged(a => ballAnimation.Colour = a.NewValue, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,6 +107,26 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
layerSpec.Rotation = -appliedRotation;
|
layerSpec.Rotation = -appliedRotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onHitObjectApplied(DrawableHitObject? drawableObject = null)
|
||||||
|
{
|
||||||
|
double frameDelay;
|
||||||
|
|
||||||
|
if (drawableObject?.HitObject != null)
|
||||||
|
{
|
||||||
|
DrawableSlider drawableSlider = (DrawableSlider)drawableObject;
|
||||||
|
|
||||||
|
frameDelay = Math.Max(
|
||||||
|
0.15 / drawableSlider.HitObject.Velocity * LegacySkinExtensions.SIXTY_FRAME_TIME,
|
||||||
|
LegacySkinExtensions.SIXTY_FRAME_TIME);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
frameDelay = LegacySkinExtensions.SIXTY_FRAME_TIME;
|
||||||
|
|
||||||
|
ballAnimation.ClearFrames();
|
||||||
|
foreach (var texture in ballTextures)
|
||||||
|
ballAnimation.AddFrame(texture, frameDelay);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState _)
|
private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState _)
|
||||||
{
|
{
|
||||||
// Gets called by slider ticks, tails, etc., leading to duplicated
|
// Gets called by slider ticks, tails, etc., leading to duplicated
|
||||||
@ -114,7 +147,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
if (parentObject != null)
|
if (parentObject != null)
|
||||||
|
{
|
||||||
|
parentObject.HitObjectApplied -= onHitObjectApplied;
|
||||||
parentObject.ApplyCustomUpdateState -= updateStateTransforms;
|
parentObject.ApplyCustomUpdateState -= updateStateTransforms;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Scale = new Vector2(SPRITE_SCALE),
|
Scale = new Vector2(SPRITE_SCALE),
|
||||||
Y = SPINNER_TOP_OFFSET + 299,
|
Y = SPINNER_TOP_OFFSET + 299,
|
||||||
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
|
},
|
||||||
spmBackground = new Sprite
|
spmBackground = new Sprite
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
Origin = Anchor.TopRight,
|
Origin = Anchor.TopRight,
|
||||||
Scale = new Vector2(SPRITE_SCALE * 0.9f),
|
Scale = new Vector2(SPRITE_SCALE * 0.9f),
|
||||||
Position = new Vector2(80, 448 + spm_hide_offset),
|
Position = new Vector2(80, 448 + spm_hide_offset),
|
||||||
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -59,13 +59,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
case OsuSkinComponents.SliderBall:
|
case OsuSkinComponents.SliderBall:
|
||||||
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "", maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE);
|
if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null)
|
||||||
|
return new LegacySliderBall(this);
|
||||||
// todo: slider ball has a custom frame delay based on velocity
|
|
||||||
// Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
|
|
||||||
|
|
||||||
if (sliderBallContent != null)
|
|
||||||
return new LegacySliderBall(sliderBallContent, this);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@ -150,10 +145,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
const float hitcircle_text_scale = 0.8f;
|
const float hitcircle_text_scale = 0.8f;
|
||||||
return new LegacySpriteText(LegacyFont.HitCircle, OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale)
|
return new LegacySpriteText(LegacyFont.HitCircle)
|
||||||
{
|
{
|
||||||
// stable applies a blanket 0.8x scale to hitcircle fonts
|
// stable applies a blanket 0.8x scale to hitcircle fonts
|
||||||
Scale = new Vector2(hitcircle_text_scale),
|
Scale = new Vector2(hitcircle_text_scale),
|
||||||
|
MaxSizePerGlyph = OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale,
|
||||||
};
|
};
|
||||||
|
|
||||||
case OsuSkinComponents.SpinnerBody:
|
case OsuSkinComponents.SpinnerBody:
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
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.Colour;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
@ -14,53 +15,54 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
|||||||
{
|
{
|
||||||
public partial class ArgonBarLine : CompositeDrawable
|
public partial class ArgonBarLine : CompositeDrawable
|
||||||
{
|
{
|
||||||
private Container majorEdgeContainer = null!;
|
|
||||||
|
|
||||||
private Bindable<bool> major = null!;
|
private Bindable<bool> major = null!;
|
||||||
|
|
||||||
|
private Box mainLine = null!;
|
||||||
|
private Drawable topAnchor = null!;
|
||||||
|
private Drawable bottomAnchor = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(DrawableHitObject drawableHitObject)
|
private void load(DrawableHitObject drawableHitObject)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
const float line_offset = 8;
|
// Avoid flickering due to no anti-aliasing of boxes by default.
|
||||||
var majorPieceSize = new Vector2(6, 20);
|
var edgeSmoothness = new Vector2(0.3f);
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
AddInternal(mainLine = new Box
|
||||||
{
|
{
|
||||||
line = new Box
|
Name = "Bar line",
|
||||||
{
|
EdgeSmoothness = edgeSmoothness,
|
||||||
RelativeSizeAxes = Axes.Both,
|
Anchor = Anchor.Centre,
|
||||||
EdgeSmoothness = new Vector2(0.5f, 0),
|
Origin = Anchor.Centre,
|
||||||
Anchor = Anchor.Centre,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Origin = Anchor.Centre,
|
});
|
||||||
},
|
|
||||||
majorEdgeContainer = new Container
|
const float major_extension = 10;
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
AddInternal(topAnchor = new Box
|
||||||
Origin = Anchor.Centre,
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
Name = "Top anchor",
|
||||||
Children = new[]
|
EdgeSmoothness = edgeSmoothness,
|
||||||
{
|
Blending = BlendingParameters.Additive,
|
||||||
new Circle
|
Anchor = Anchor.TopCentre,
|
||||||
{
|
Origin = Anchor.BottomCentre,
|
||||||
Name = "Top line",
|
Height = major_extension,
|
||||||
Anchor = Anchor.TopCentre,
|
RelativeSizeAxes = Axes.X,
|
||||||
Origin = Anchor.BottomCentre,
|
Colour = ColourInfo.GradientVertical(Colour4.Transparent, Colour4.White),
|
||||||
Size = majorPieceSize,
|
});
|
||||||
Y = -line_offset,
|
|
||||||
},
|
AddInternal(bottomAnchor = new Box
|
||||||
new Circle
|
{
|
||||||
{
|
Name = "Bottom anchor",
|
||||||
Name = "Bottom line",
|
EdgeSmoothness = edgeSmoothness,
|
||||||
Anchor = Anchor.BottomCentre,
|
Blending = BlendingParameters.Additive,
|
||||||
Origin = Anchor.TopCentre,
|
Anchor = Anchor.BottomCentre,
|
||||||
Size = majorPieceSize,
|
Origin = Anchor.TopCentre,
|
||||||
Y = line_offset,
|
Height = major_extension,
|
||||||
},
|
RelativeSizeAxes = Axes.X,
|
||||||
}
|
Colour = ColourInfo.GradientVertical(Colour4.White, Colour4.Transparent),
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
|
||||||
major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy();
|
major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy();
|
||||||
}
|
}
|
||||||
@ -71,13 +73,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
|||||||
major.BindValueChanged(updateMajor, true);
|
major.BindValueChanged(updateMajor, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Box line = null!;
|
|
||||||
|
|
||||||
private void updateMajor(ValueChangedEvent<bool> major)
|
private void updateMajor(ValueChangedEvent<bool> major)
|
||||||
{
|
{
|
||||||
line.Alpha = major.NewValue ? 1f : 0.5f;
|
mainLine.Alpha = major.NewValue ? 1f : 0.5f;
|
||||||
line.Width = major.NewValue ? 1 : 0.5f;
|
topAnchor.Alpha = bottomAnchor.Alpha = major.NewValue ? mainLine.Alpha * 0.3f : 0;
|
||||||
majorEdgeContainer.Alpha = major.NewValue ? 1 : 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,34 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDecodeLegacyOnlineID()
|
||||||
|
{
|
||||||
|
var decoder = new TestLegacyScoreDecoder();
|
||||||
|
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Replays/taiko-replay-with-legacy-online-id.osr"))
|
||||||
|
{
|
||||||
|
var score = decoder.Parse(resourceStream);
|
||||||
|
|
||||||
|
Assert.That(score.ScoreInfo.OnlineID, Is.EqualTo(-1));
|
||||||
|
Assert.That(score.ScoreInfo.LegacyOnlineID, Is.EqualTo(255));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDecodeNewOnlineID()
|
||||||
|
{
|
||||||
|
var decoder = new TestLegacyScoreDecoder();
|
||||||
|
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Replays/taiko-replay-with-new-online-id.osr"))
|
||||||
|
{
|
||||||
|
var score = decoder.Parse(resourceStream);
|
||||||
|
|
||||||
|
Assert.That(score.ScoreInfo.OnlineID, Is.EqualTo(258));
|
||||||
|
Assert.That(score.ScoreInfo.LegacyOnlineID, Is.EqualTo(-1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase(3, true)]
|
[TestCase(3, true)]
|
||||||
[TestCase(6, false)]
|
[TestCase(6, false)]
|
||||||
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)]
|
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)]
|
||||||
|
@ -287,5 +287,26 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + loop_duration));
|
Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + loop_duration));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestVideoAndBackgroundEventsDoNotAffectStoryboardBounds()
|
||||||
|
{
|
||||||
|
var decoder = new LegacyStoryboardDecoder();
|
||||||
|
|
||||||
|
using var resStream = TestResources.OpenResource("video-background-events-ignored.osb");
|
||||||
|
using var stream = new LineBufferedReader(resStream);
|
||||||
|
|
||||||
|
var storyboard = decoder.Decode(stream);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(storyboard.GetLayer(@"Video").Elements, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(storyboard.GetLayer(@"Video").Elements.Single(), Is.InstanceOf<StoryboardVideo>());
|
||||||
|
Assert.That(storyboard.GetLayer(@"Video").Elements.Single().StartTime, Is.EqualTo(-5678));
|
||||||
|
|
||||||
|
Assert.That(storyboard.EarliestEventTime, Is.Null);
|
||||||
|
Assert.That(storyboard.LatestEventTime, Is.Null);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,6 +108,28 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
AddAssert("gameplay clock time = 10000", () => gameplayClockContainer.CurrentTime, () => Is.EqualTo(10000).Within(10f));
|
AddAssert("gameplay clock time = 10000", () => gameplayClockContainer.CurrentTime, () => Is.EqualTo(10000).Within(10f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestStopUsingBeatmapClock()
|
||||||
|
{
|
||||||
|
ClockBackedTestWorkingBeatmap working = null;
|
||||||
|
MasterGameplayClockContainer gameplayClockContainer = null;
|
||||||
|
BindableDouble frequencyAdjustment = new BindableDouble(2);
|
||||||
|
|
||||||
|
AddStep("create container", () =>
|
||||||
|
{
|
||||||
|
working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio);
|
||||||
|
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
|
||||||
|
|
||||||
|
gameplayClockContainer.Reset(startClock: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("apply frequency adjustment", () => gameplayClockContainer.AdjustmentsFromMods.AddAdjustment(AdjustableProperty.Frequency, frequencyAdjustment));
|
||||||
|
AddAssert("track frequency changed", () => working.Track.AggregateFrequency.Value, () => Is.EqualTo(2));
|
||||||
|
|
||||||
|
AddStep("stop using beatmap clock", () => gameplayClockContainer.StopUsingBeatmapClock());
|
||||||
|
AddAssert("frequency adjustment unapplied", () => working.Track.AggregateFrequency.Value, () => Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
localConfig?.Dispose();
|
localConfig?.Dispose();
|
||||||
|
@ -2,10 +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;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Filter;
|
using osu.Game.Rulesets.Filter;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
|
using osu.Game.Screens.Select.Carousel;
|
||||||
using osu.Game.Screens.Select.Filter;
|
using osu.Game.Screens.Select.Filter;
|
||||||
|
|
||||||
namespace osu.Game.Tests.NonVisual.Filtering
|
namespace osu.Game.Tests.NonVisual.Filtering
|
||||||
@ -382,6 +384,57 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText.Trim());
|
Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText.Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("[1]", new[] { 0 })]
|
||||||
|
[TestCase("[1", new[] { 0 })]
|
||||||
|
[TestCase("My[Favourite", new[] { 2 })]
|
||||||
|
[TestCase("My[Favourite]", new[] { 2 })]
|
||||||
|
[TestCase("My[Favourite]Song", new[] { 2 })]
|
||||||
|
[TestCase("Favourite]", new[] { 2 })]
|
||||||
|
[TestCase("[Diff", new[] { 0, 1, 3, 4, 6 })]
|
||||||
|
[TestCase("[Diff]", new[] { 0, 1, 3, 4, 6 })]
|
||||||
|
[TestCase("[Favourite]", new[] { 3 })]
|
||||||
|
[TestCase("Title1 [Diff]", new[] { 0, 1 })]
|
||||||
|
[TestCase("Title1[Diff]", new int[] { })]
|
||||||
|
[TestCase("[diff ]with]", new[] { 4 })]
|
||||||
|
[TestCase("[diff ]with [[ brackets]]]]", new[] { 4 })]
|
||||||
|
[TestCase("[Diff in title]", new int[] { })]
|
||||||
|
[TestCase("[Diff in diff]", new[] { 6 })]
|
||||||
|
[TestCase("diff=Diff", new[] { 0, 1, 3, 4, 6 })]
|
||||||
|
[TestCase("diff=Diff1", new[] { 0 })]
|
||||||
|
[TestCase("diff=\"Diff\"", new[] { 3, 4, 6 })]
|
||||||
|
[TestCase("diff=!\"Diff\"", new int[] { })]
|
||||||
|
public void TestDifficultySearch(string query, int[] expectedBeatmapIndexes)
|
||||||
|
{
|
||||||
|
var carouselBeatmaps = (((string title, string difficultyName)[])new[]
|
||||||
|
{
|
||||||
|
("Title1", "Diff1"),
|
||||||
|
("Title1", "Diff2"),
|
||||||
|
("My[Favourite]Song", "Expert"),
|
||||||
|
("Title", "My Favourite Diff"),
|
||||||
|
("Another One", "diff ]with [[ brackets]]]"),
|
||||||
|
("Diff in title", "a"),
|
||||||
|
("a", "Diff in diff"),
|
||||||
|
}).Select(info => new CarouselBeatmap(new BeatmapInfo
|
||||||
|
{
|
||||||
|
Metadata = new BeatmapMetadata
|
||||||
|
{
|
||||||
|
Title = info.title
|
||||||
|
},
|
||||||
|
DifficultyName = info.difficultyName
|
||||||
|
})).ToList();
|
||||||
|
|
||||||
|
var criteria = new FilterCriteria();
|
||||||
|
|
||||||
|
FilterQueryParser.ApplyQueries(criteria, query);
|
||||||
|
carouselBeatmaps.ForEach(b => b.Filter(criteria));
|
||||||
|
|
||||||
|
int[] visibleBeatmaps = carouselBeatmaps
|
||||||
|
.Where(b => !b.Filtered.Value)
|
||||||
|
.Select(b => carouselBeatmaps.IndexOf(b)).ToArray();
|
||||||
|
|
||||||
|
Assert.That(visibleBeatmaps, Is.EqualTo(expectedBeatmapIndexes));
|
||||||
|
}
|
||||||
|
|
||||||
private class CustomFilterCriteria : IRulesetFilterCriteria
|
private class CustomFilterCriteria : IRulesetFilterCriteria
|
||||||
{
|
{
|
||||||
public string? CustomValue { get; set; }
|
public string? CustomValue { get; set; }
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
osu file format v14
|
||||||
|
|
||||||
|
[Events]
|
||||||
|
0,-1234,"BG.jpg",0,0
|
||||||
|
Video,-5678,"Video.avi",0,0
|
@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
InputManager.MoveMouseTo(button);
|
InputManager.MoveMouseTo(button);
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveBeforeGameplayTestDialog);
|
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
|
||||||
|
|
||||||
AddStep("dismiss prompt", () =>
|
AddStep("dismiss prompt", () =>
|
||||||
{
|
{
|
||||||
@ -165,7 +165,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
InputManager.MoveMouseTo(button);
|
InputManager.MoveMouseTo(button);
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveBeforeGameplayTestDialog);
|
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
|
||||||
|
|
||||||
AddStep("save changes", () => DialogOverlay.CurrentDialog.PerformOkAction());
|
AddStep("save changes", () => DialogOverlay.CurrentDialog.PerformOkAction());
|
||||||
|
|
||||||
|
@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
OnlineID = hasOnlineId ? online_score_id : 0,
|
OnlineID = hasOnlineId ? online_score_id : 0,
|
||||||
Ruleset = new OsuRuleset().RulesetInfo,
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
BeatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(),
|
BeatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(),
|
||||||
Hash = replayAvailable ? "online" : string.Empty,
|
HasOnlineReplay = replayAvailable,
|
||||||
User = new APIUser
|
User = new APIUser
|
||||||
{
|
{
|
||||||
Id = 39828,
|
Id = 39828,
|
||||||
|
@ -7,6 +7,7 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Configuration;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -16,6 +17,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Collections;
|
using osu.Game.Collections;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.Leaderboards;
|
using osu.Game.Online.Leaderboards;
|
||||||
@ -34,6 +36,7 @@ using osu.Game.Screens.OnlinePlay.Playlists;
|
|||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
|
using osu.Game.Screens.Select.Carousel;
|
||||||
using osu.Game.Screens.Select.Leaderboards;
|
using osu.Game.Screens.Select.Leaderboards;
|
||||||
using osu.Game.Screens.Select.Options;
|
using osu.Game.Screens.Select.Options;
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
@ -165,6 +168,41 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
ConfirmAtMainMenu();
|
ConfirmAtMainMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSongSelectScrollHandling()
|
||||||
|
{
|
||||||
|
TestPlaySongSelect songSelect = null;
|
||||||
|
double scrollPosition = 0;
|
||||||
|
|
||||||
|
AddStep("set game volume to max", () => Game.Dependencies.Get<FrameworkConfigManager>().SetValue(FrameworkSetting.VolumeUniversal, 1d));
|
||||||
|
AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType<VolumeOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||||
|
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||||
|
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||||
|
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||||
|
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||||
|
|
||||||
|
AddStep("store scroll position", () => scrollPosition = getCarouselScrollPosition());
|
||||||
|
|
||||||
|
AddStep("move to left side", () => InputManager.MoveMouseTo(
|
||||||
|
songSelect.ChildrenOfType<Screens.Select.SongSelect.LeftSideInteractionContainer>().Single().ScreenSpaceDrawQuad.TopLeft + new Vector2(1)));
|
||||||
|
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1));
|
||||||
|
AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition));
|
||||||
|
|
||||||
|
AddRepeatStep("alt-scroll down", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.AltLeft);
|
||||||
|
InputManager.ScrollVerticalBy(-1);
|
||||||
|
InputManager.ReleaseKey(Key.AltLeft);
|
||||||
|
}, 5);
|
||||||
|
AddAssert("game volume decreased", () => Game.Dependencies.Get<FrameworkConfigManager>().Get<double>(FrameworkSetting.VolumeUniversal), () => Is.LessThan(1));
|
||||||
|
|
||||||
|
AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType<BeatmapCarousel>().Single()));
|
||||||
|
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1));
|
||||||
|
AddAssert("carousel moved", getCarouselScrollPosition, () => Is.Not.EqualTo(scrollPosition));
|
||||||
|
|
||||||
|
double getCarouselScrollPosition() => Game.ChildrenOfType<UserTrackingScrollContainer<DrawableCarouselItem>>().Single().Current;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This tests that the F1 key will open the mod select overlay, and not be handled / blocked by the music controller (which has the same default binding
|
/// This tests that the F1 key will open the mod select overlay, and not be handled / blocked by the music controller (which has the same default binding
|
||||||
/// but should be handled *after* song select).
|
/// but should be handled *after* song select).
|
||||||
@ -209,6 +247,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddStep("end spectator before retry", () => Game.SpectatorClient.EndPlaying(player.GameplayState));
|
AddStep("end spectator before retry", () => Game.SpectatorClient.EndPlaying(player.GameplayState));
|
||||||
|
|
||||||
AddStep("attempt to retry", () => player.ChildrenOfType<HotkeyRetryOverlay>().First().Action());
|
AddStep("attempt to retry", () => player.ChildrenOfType<HotkeyRetryOverlay>().First().Action());
|
||||||
|
AddAssert("old player score marked failed", () => player.Score.ScoreInfo.Rank, () => Is.EqualTo(ScoreRank.F));
|
||||||
AddUntilStep("wait for old player gone", () => Game.ScreenStack.CurrentScreen != player);
|
AddUntilStep("wait for old player gone", () => Game.ScreenStack.CurrentScreen != player);
|
||||||
|
|
||||||
AddUntilStep("get new player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
|
AddUntilStep("get new player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
|
||||||
@ -221,6 +260,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
var getOriginalPlayer = playToCompletion();
|
var getOriginalPlayer = playToCompletion();
|
||||||
|
|
||||||
AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType<HotkeyRetryOverlay>().First().Action());
|
AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType<HotkeyRetryOverlay>().First().Action());
|
||||||
|
AddAssert("original play isn't failed", () => getOriginalPlayer().Score.ScoreInfo.Rank, () => Is.Not.EqualTo(ScoreRank.F));
|
||||||
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player);
|
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
@ -18,6 +19,7 @@ using osu.Game.Rulesets.Osu.Mods;
|
|||||||
using osu.Game.Screens.Edit.Components;
|
using osu.Game.Screens.Edit.Components;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||||
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@ -31,7 +33,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
private SkinEditor skinEditor => Game.ChildrenOfType<SkinEditor>().FirstOrDefault();
|
private SkinEditor skinEditor => Game.ChildrenOfType<SkinEditor>().FirstOrDefault();
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestEditComponentDuringGameplay()
|
public void TestEditComponentFromGameplayScene()
|
||||||
{
|
{
|
||||||
advanceToSongSelect();
|
advanceToSongSelect();
|
||||||
openSkinEditor();
|
openSkinEditor();
|
||||||
@ -69,6 +71,28 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
|
AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMutateProtectedSkinDuringGameplay()
|
||||||
|
{
|
||||||
|
advanceToSongSelect();
|
||||||
|
AddStep("set default skin", () => Game.Dependencies.Get<SkinManager>().CurrentSkinInfo.SetDefault());
|
||||||
|
|
||||||
|
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||||
|
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||||
|
|
||||||
|
AddStep("enable NF", () => Game.SelectedMods.Value = new[] { new OsuModNoFail() });
|
||||||
|
AddStep("enter gameplay", () => InputManager.Key(Key.Enter));
|
||||||
|
|
||||||
|
AddUntilStep("wait for player", () =>
|
||||||
|
{
|
||||||
|
DismissAnyNotifications();
|
||||||
|
return Game.ScreenStack.CurrentScreen is Player;
|
||||||
|
});
|
||||||
|
|
||||||
|
openSkinEditor();
|
||||||
|
AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get<SkinManager>().CurrentSkin.Value.SkinInfo.Value.Protected);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestComponentsDeselectedOnSkinEditorHide()
|
public void TestComponentsDeselectedOnSkinEditorHide()
|
||||||
{
|
{
|
||||||
|
@ -64,7 +64,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
new[] { "Plain", "This is plain comment" },
|
new[] { "Plain", "This is plain comment" },
|
||||||
new[] { "Pinned", "This is pinned comment" },
|
new[] { "Pinned", "This is pinned comment" },
|
||||||
new[] { "Link", "Please visit https://osu.ppy.sh" },
|
new[] { "Link", "Please visit https://osu.ppy.sh" },
|
||||||
|
new[] { "Big Image", "" },
|
||||||
|
new[] { "Small Image", "" },
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"Heading", @"# Heading 1
|
"Heading", @"# Heading 1
|
||||||
|
@ -362,7 +362,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
{
|
{
|
||||||
var score = TestResources.CreateTestScoreInfo();
|
var score = TestResources.CreateTestScoreInfo();
|
||||||
score.TotalScore += 10 - i;
|
score.TotalScore += 10 - i;
|
||||||
score.Hash = $"test{i}";
|
score.HasOnlineReplay = true;
|
||||||
scores.Add(score);
|
scores.Add(score);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,26 +18,24 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
[Cached]
|
[Cached]
|
||||||
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple);
|
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||||
|
|
||||||
private readonly BindableDouble current = new BindableDouble(5)
|
|
||||||
{
|
|
||||||
Precision = 0.1f,
|
|
||||||
MinValue = 0,
|
|
||||||
MaxValue = 15
|
|
||||||
};
|
|
||||||
|
|
||||||
private RoundedSliderBar<double> slider = null!;
|
private RoundedSliderBar<double> slider = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[SetUpSteps]
|
||||||
private void load()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
Child = slider = new RoundedSliderBar<double>
|
AddStep("create slider", () => Child = slider = new RoundedSliderBar<double>
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Current = current,
|
Current = new BindableDouble(5)
|
||||||
|
{
|
||||||
|
Precision = 0.1,
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 15
|
||||||
|
},
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Width = 0.4f
|
Width = 0.4f
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -55,5 +53,22 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
AddAssert("slider is default", () => slider.Current.IsDefault);
|
AddAssert("slider is default", () => slider.Current.IsDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNubDoubleClickOnDisabledSliderDoesNothing()
|
||||||
|
{
|
||||||
|
AddStep("set slider to 1", () => slider.Current.Value = 1);
|
||||||
|
AddStep("disable slider", () => slider.Current.Disabled = true);
|
||||||
|
|
||||||
|
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType<Nub>().Single()));
|
||||||
|
|
||||||
|
AddStep("double click nub", () =>
|
||||||
|
{
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,26 +18,24 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
[Cached]
|
[Cached]
|
||||||
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple);
|
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||||
|
|
||||||
private readonly BindableDouble current = new BindableDouble(5)
|
|
||||||
{
|
|
||||||
Precision = 0.1f,
|
|
||||||
MinValue = 0,
|
|
||||||
MaxValue = 15
|
|
||||||
};
|
|
||||||
|
|
||||||
private ShearedSliderBar<double> slider = null!;
|
private ShearedSliderBar<double> slider = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[SetUpSteps]
|
||||||
private void load()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
Child = slider = new ShearedSliderBar<double>
|
AddStep("create slider", () => Child = slider = new ShearedSliderBar<double>
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Current = current,
|
Current = new BindableDouble(5)
|
||||||
|
{
|
||||||
|
Precision = 0.1,
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 15
|
||||||
|
},
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Width = 0.4f
|
Width = 0.4f
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -55,5 +53,22 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
AddAssert("slider is default", () => slider.Current.IsDefault);
|
AddAssert("slider is default", () => slider.Current.IsDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNubDoubleClickOnDisabledSliderDoesNothing()
|
||||||
|
{
|
||||||
|
AddStep("set slider to 1", () => slider.Current.Value = 1);
|
||||||
|
AddStep("disable slider", () => slider.Current.Disabled = true);
|
||||||
|
|
||||||
|
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType<ShearedNub>().Single()));
|
||||||
|
|
||||||
|
AddStep("double click nub", () =>
|
||||||
|
{
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// 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;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
@ -10,7 +11,7 @@ namespace osu.Game.Tournament.Components
|
|||||||
{
|
{
|
||||||
public partial class DateTextBox : SettingsTextBox
|
public partial class DateTextBox : SettingsTextBox
|
||||||
{
|
{
|
||||||
private readonly BindableWithCurrent<DateTimeOffset> current = new BindableWithCurrent<DateTimeOffset>();
|
private readonly BindableWithCurrent<DateTimeOffset> current = new BindableWithCurrent<DateTimeOffset>(DateTimeOffset.Now);
|
||||||
|
|
||||||
public new Bindable<DateTimeOffset>? Current
|
public new Bindable<DateTimeOffset>? Current
|
||||||
{
|
{
|
||||||
@ -23,13 +24,13 @@ namespace osu.Game.Tournament.Components
|
|||||||
base.Current = new Bindable<string>(string.Empty);
|
base.Current = new Bindable<string>(string.Empty);
|
||||||
|
|
||||||
current.BindValueChanged(dto =>
|
current.BindValueChanged(dto =>
|
||||||
base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true);
|
base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", DateTimeFormatInfo.InvariantInfo), true);
|
||||||
|
|
||||||
((OsuTextBox)Control).OnCommit += (sender, _) =>
|
((OsuTextBox)Control).OnCommit += (sender, _) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
current.Value = DateTimeOffset.Parse(sender.Text);
|
current.Value = DateTimeOffset.Parse(sender.Text, DateTimeFormatInfo.InvariantInfo);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
@ -1,7 +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.
|
||||||
|
|
||||||
using System;
|
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Tournament
|
namespace osu.Game.Tournament
|
||||||
@ -519,9 +518,6 @@ namespace osu.Game.Tournament
|
|||||||
case CountryCode.KE:
|
case CountryCode.KE:
|
||||||
return "KEN";
|
return "KEN";
|
||||||
|
|
||||||
case CountryCode.SS:
|
|
||||||
return "SSD";
|
|
||||||
|
|
||||||
case CountryCode.SR:
|
case CountryCode.SR:
|
||||||
return "SUR";
|
return "SUR";
|
||||||
|
|
||||||
@ -763,7 +759,7 @@ namespace osu.Game.Tournament
|
|||||||
return "MOZ";
|
return "MOZ";
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException(nameof(country));
|
return country.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,15 @@ namespace osu.Game.Database
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A model that contains a list of files it is responsible for.
|
/// A model that contains a list of files it is responsible for.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IHasRealmFiles
|
public interface IHasRealmFiles : IHasNamedFiles
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Available files in this model, with locally filenames.
|
/// Available files in this model, with locally filenames.
|
||||||
/// When performing lookups, consider using <see cref="BeatmapSetInfoExtensions.GetFile"/> or <see cref="BeatmapSetInfoExtensions.GetPathForFile"/> to do case-insensitive lookups.
|
/// When performing lookups, consider using <see cref="BeatmapSetInfoExtensions.GetFile"/> or <see cref="BeatmapSetInfoExtensions.GetPathForFile"/> to do case-insensitive lookups.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IList<RealmNamedFileUsage> Files { get; }
|
new IList<RealmNamedFileUsage> Files { get; }
|
||||||
|
|
||||||
|
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A combined hash representing the model, based on the files it contains.
|
/// A combined hash representing the model, based on the files it contains.
|
||||||
|
@ -21,5 +21,11 @@ namespace osu.Game.Database
|
|||||||
/// Whether this import should use hard links rather than file copy operations if available.
|
/// Whether this import should use hard links rather than file copy operations if available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool PreferHardLinks { get; set; }
|
public bool PreferHardLinks { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If set to <see langword="true"/>, this import will not respect <see cref="RealmArchiveModelImporter{TModel}.PauseImports"/>.
|
||||||
|
/// This is useful for cases where an import <em>must</em> complete even if gameplay is in progress.
|
||||||
|
/// </summary>
|
||||||
|
public bool ImportImmediately { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,8 +87,9 @@ namespace osu.Game.Database
|
|||||||
/// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding.
|
/// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding.
|
||||||
/// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures.
|
/// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures.
|
||||||
/// 35 2023-10-16 Clear key combinations of keybindings that are assigned to more than one action in a given settings section.
|
/// 35 2023-10-16 Clear key combinations of keybindings that are assigned to more than one action in a given settings section.
|
||||||
|
/// 36 2023-10-26 Add LegacyOnlineID to ScoreInfo. Move osu_scores_*_high IDs stored in OnlineID to LegacyOnlineID. Reset anomalous OnlineIDs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int schema_version = 35;
|
private const int schema_version = 36;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||||
@ -1075,6 +1076,24 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 36:
|
||||||
|
{
|
||||||
|
foreach (var score in migration.NewRealm.All<ScoreInfo>())
|
||||||
|
{
|
||||||
|
if (score.OnlineID > 0)
|
||||||
|
{
|
||||||
|
score.LegacyOnlineID = score.OnlineID;
|
||||||
|
score.OnlineID = -1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
score.LegacyOnlineID = score.OnlineID = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
||||||
|
@ -261,7 +261,7 @@ namespace osu.Game.Database
|
|||||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||||
public virtual Live<TModel>? ImportModel(TModel item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => Realm.Run(realm =>
|
public virtual Live<TModel>? ImportModel(TModel item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => Realm.Run(realm =>
|
||||||
{
|
{
|
||||||
pauseIfNecessary(cancellationToken);
|
pauseIfNecessary(parameters, cancellationToken);
|
||||||
|
|
||||||
TModel? existing;
|
TModel? existing;
|
||||||
|
|
||||||
@ -560,9 +560,9 @@ namespace osu.Game.Database
|
|||||||
/// <returns>Whether to perform deletion.</returns>
|
/// <returns>Whether to perform deletion.</returns>
|
||||||
protected virtual bool ShouldDeleteArchive(string path) => false;
|
protected virtual bool ShouldDeleteArchive(string path) => false;
|
||||||
|
|
||||||
private void pauseIfNecessary(CancellationToken cancellationToken)
|
private void pauseIfNecessary(ImportParameters importParameters, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!PauseImports)
|
if (!PauseImports || importParameters.ImportImmediately)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Logger.Log($@"{GetType().Name} is being paused.");
|
Logger.Log($@"{GetType().Name} is being paused.");
|
||||||
|
@ -114,8 +114,24 @@ namespace osu.Game.Extensions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="instance">The instance to compare.</param>
|
/// <param name="instance">The instance to compare.</param>
|
||||||
/// <param name="other">The other instance to compare against.</param>
|
/// <param name="other">The other instance to compare against.</param>
|
||||||
/// <returns>Whether online IDs match. If either instance is missing an online ID, this will return false.</returns>
|
/// <returns>
|
||||||
public static bool MatchesOnlineID(this IScoreInfo? instance, IScoreInfo? other) => matchesOnlineID(instance, other);
|
/// Whether online IDs match.
|
||||||
|
/// Both <see cref="IHasOnlineID{T}.OnlineID"/> and <see cref="IScoreInfo.LegacyOnlineID"/> are checked, in that order.
|
||||||
|
/// If either instance is missing an online ID, this will return false.
|
||||||
|
/// </returns>
|
||||||
|
public static bool MatchesOnlineID(this IScoreInfo? instance, IScoreInfo? other)
|
||||||
|
{
|
||||||
|
if (matchesOnlineID(instance, other))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (instance == null || other == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (instance.LegacyOnlineID < 0 || other.LegacyOnlineID < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return instance.LegacyOnlineID.Equals(other.LegacyOnlineID);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool matchesOnlineID(this IHasOnlineID<long>? instance, IHasOnlineID<long>? other)
|
private static bool matchesOnlineID(this IHasOnlineID<long>? instance, IHasOnlineID<long>? other)
|
||||||
{
|
{
|
||||||
|
@ -98,7 +98,11 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
Origin = Anchor.TopCentre,
|
Origin = Anchor.TopCentre,
|
||||||
RelativePositionAxes = Axes.X,
|
RelativePositionAxes = Axes.X,
|
||||||
Current = { Value = true },
|
Current = { Value = true },
|
||||||
OnDoubleClicked = () => Current.SetDefault(),
|
OnDoubleClicked = () =>
|
||||||
|
{
|
||||||
|
if (!Current.Disabled)
|
||||||
|
Current.SetDefault();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hoverClickSounds = new HoverClickSounds()
|
hoverClickSounds = new HoverClickSounds()
|
||||||
|
@ -101,7 +101,11 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
Origin = Anchor.TopCentre,
|
Origin = Anchor.TopCentre,
|
||||||
RelativePositionAxes = Axes.X,
|
RelativePositionAxes = Axes.X,
|
||||||
Current = { Value = true },
|
Current = { Value = true },
|
||||||
OnDoubleClicked = () => Current.SetDefault(),
|
OnDoubleClicked = () =>
|
||||||
|
{
|
||||||
|
if (!Current.Disabled)
|
||||||
|
Current.SetDefault();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hoverClickSounds = new HoverClickSounds()
|
hoverClickSounds = new HoverClickSounds()
|
||||||
|
@ -149,6 +149,7 @@ namespace osu.Game.Input.Bindings
|
|||||||
new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
|
new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
|
||||||
new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene),
|
new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene),
|
||||||
new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry),
|
new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry),
|
||||||
|
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.QuickRetry),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit),
|
new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit),
|
||||||
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed),
|
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed),
|
||||||
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed),
|
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed),
|
||||||
|
@ -7,16 +7,16 @@ using System.Linq;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Database;
|
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests.Responses
|
namespace osu.Game.Online.API.Requests.Responses
|
||||||
{
|
{
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class SoloScoreInfo : IHasOnlineID<long>
|
public class SoloScoreInfo : IScoreInfo
|
||||||
{
|
{
|
||||||
[JsonProperty("beatmap_id")]
|
[JsonProperty("beatmap_id")]
|
||||||
public int BeatmapID { get; set; }
|
public int BeatmapID { get; set; }
|
||||||
@ -138,6 +138,18 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region IScoreInfo
|
||||||
|
|
||||||
|
public long OnlineID => (long?)ID ?? -1;
|
||||||
|
|
||||||
|
IUser IScoreInfo.User => User!;
|
||||||
|
DateTimeOffset IScoreInfo.Date => EndedAt;
|
||||||
|
long IScoreInfo.LegacyOnlineID => (long?)LegacyScoreId ?? -1;
|
||||||
|
IBeatmapInfo IScoreInfo.Beatmap => Beatmap!;
|
||||||
|
IRulesetInfo IScoreInfo.Ruleset => Beatmap!.Ruleset;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
public override string ToString() => $"score_id: {ID} user_id: {UserID}";
|
public override string ToString() => $"score_id: {ID} user_id: {UserID}";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -178,6 +190,7 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
var score = new ScoreInfo
|
var score = new ScoreInfo
|
||||||
{
|
{
|
||||||
OnlineID = OnlineID,
|
OnlineID = OnlineID,
|
||||||
|
LegacyOnlineID = (long?)LegacyScoreId ?? -1,
|
||||||
User = User ?? new APIUser { Id = UserID },
|
User = User ?? new APIUser { Id = UserID },
|
||||||
BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
|
BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
|
||||||
Ruleset = new RulesetInfo { OnlineID = RulesetID },
|
Ruleset = new RulesetInfo { OnlineID = RulesetID },
|
||||||
@ -189,7 +202,7 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
Statistics = Statistics,
|
Statistics = Statistics,
|
||||||
MaximumStatistics = MaximumStatistics,
|
MaximumStatistics = MaximumStatistics,
|
||||||
Date = EndedAt,
|
Date = EndedAt,
|
||||||
Hash = HasReplay ? "online" : string.Empty, // TODO: temporary?
|
HasOnlineReplay = HasReplay,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
PP = PP,
|
PP = PP,
|
||||||
};
|
};
|
||||||
@ -223,7 +236,5 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||||
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||||
};
|
};
|
||||||
|
|
||||||
public long OnlineID => (long?)ID ?? -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,9 @@ namespace osu.Game.Online.Rooms
|
|||||||
[JsonProperty("position")]
|
[JsonProperty("position")]
|
||||||
public int? Position { get; set; }
|
public int? Position { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("has_replay")]
|
||||||
|
public bool HasReplay { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Any scores in the room around this score.
|
/// Any scores in the room around this score.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -84,7 +87,7 @@ namespace osu.Game.Online.Rooms
|
|||||||
User = User,
|
User = User,
|
||||||
Accuracy = Accuracy,
|
Accuracy = Accuracy,
|
||||||
Date = EndedAt,
|
Date = EndedAt,
|
||||||
Hash = string.Empty, // todo: temporary?
|
HasOnlineReplay = HasReplay,
|
||||||
Rank = Rank,
|
Rank = Rank,
|
||||||
Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty<Mod>(),
|
Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty<Mod>(),
|
||||||
Position = Position,
|
Position = Position,
|
||||||
|
@ -39,7 +39,8 @@ namespace osu.Game.Online
|
|||||||
var scoreInfo = new ScoreInfo
|
var scoreInfo = new ScoreInfo
|
||||||
{
|
{
|
||||||
ID = TrackedItem.ID,
|
ID = TrackedItem.ID,
|
||||||
OnlineID = TrackedItem.OnlineID
|
OnlineID = TrackedItem.OnlineID,
|
||||||
|
LegacyOnlineID = TrackedItem.LegacyOnlineID
|
||||||
};
|
};
|
||||||
|
|
||||||
Downloader.DownloadBegan += downloadBegan;
|
Downloader.DownloadBegan += downloadBegan;
|
||||||
@ -47,6 +48,7 @@ namespace osu.Game.Online
|
|||||||
|
|
||||||
realmSubscription = realm.RegisterForNotifications(r => r.All<ScoreInfo>().Where(s =>
|
realmSubscription = realm.RegisterForNotifications(r => r.All<ScoreInfo>().Where(s =>
|
||||||
((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID)
|
((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID)
|
||||||
|
|| (s.LegacyOnlineID > 0 && s.LegacyOnlineID == TrackedItem.LegacyOnlineID)
|
||||||
|| (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash))
|
|| (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash))
|
||||||
&& !s.DeletePending), (items, _) =>
|
&& !s.DeletePending), (items, _) =>
|
||||||
{
|
{
|
||||||
|
@ -678,6 +678,9 @@ namespace osu.Game
|
|||||||
if (score.OnlineID > 0)
|
if (score.OnlineID > 0)
|
||||||
databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID);
|
databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID);
|
||||||
|
|
||||||
|
if (score.LegacyOnlineID > 0)
|
||||||
|
databasedScoreInfo ??= ScoreManager.Query(s => s.LegacyOnlineID == score.LegacyOnlineID);
|
||||||
|
|
||||||
if (score is ScoreInfo scoreInfo)
|
if (score is ScoreInfo scoreInfo)
|
||||||
databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == scoreInfo.Hash);
|
databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == scoreInfo.Hash);
|
||||||
|
|
||||||
|
@ -2,8 +2,14 @@
|
|||||||
// 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 Markdig.Syntax;
|
using Markdig.Syntax;
|
||||||
|
using Markdig.Syntax.Inlines;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Containers.Markdown;
|
using osu.Framework.Graphics.Containers.Markdown;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Game.Graphics.Containers.Markdown;
|
using osu.Game.Graphics.Containers.Markdown;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Comments
|
namespace osu.Game.Overlays.Comments
|
||||||
{
|
{
|
||||||
@ -16,6 +22,8 @@ namespace osu.Game.Overlays.Comments
|
|||||||
|
|
||||||
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock);
|
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock);
|
||||||
|
|
||||||
|
public override MarkdownTextFlowContainer CreateTextFlow() => new CommentMarkdownTextFlowContainer();
|
||||||
|
|
||||||
private partial class CommentMarkdownHeading : OsuMarkdownHeading
|
private partial class CommentMarkdownHeading : OsuMarkdownHeading
|
||||||
{
|
{
|
||||||
public CommentMarkdownHeading(HeadingBlock headingBlock)
|
public CommentMarkdownHeading(HeadingBlock headingBlock)
|
||||||
@ -40,5 +48,64 @@ namespace osu.Game.Overlays.Comments
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private partial class CommentMarkdownTextFlowContainer : MarkdownTextFlowContainer
|
||||||
|
{
|
||||||
|
protected override void AddImage(LinkInline linkInline) => AddDrawable(new CommentMarkdownImage(linkInline.Url));
|
||||||
|
|
||||||
|
private partial class CommentMarkdownImage : MarkdownImage
|
||||||
|
{
|
||||||
|
public CommentMarkdownImage(string url)
|
||||||
|
: base(url)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private DelayedLoadWrapper wrapper = null!;
|
||||||
|
|
||||||
|
protected override Drawable CreateContent(string url) => wrapper = new DelayedLoadWrapper(CreateImageContainer(url));
|
||||||
|
|
||||||
|
protected override ImageContainer CreateImageContainer(string url)
|
||||||
|
{
|
||||||
|
var container = new CommentImageContainer(url);
|
||||||
|
container.OnLoadComplete += d =>
|
||||||
|
{
|
||||||
|
// The size of DelayedLoadWrapper depends on AutoSizeAxes of it's content.
|
||||||
|
// But since it's set to None, we need to specify the size here manually.
|
||||||
|
wrapper.Size = container.Size;
|
||||||
|
d.FadeInFromZero(300, Easing.OutQuint);
|
||||||
|
};
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class CommentImageContainer : ImageContainer
|
||||||
|
{
|
||||||
|
// https://github.com/ppy/osu-web/blob/3bd0f406dc78d60b356d955cd4201f8c3e1cca09/resources/css/bem/osu-md.less#L36
|
||||||
|
// Web version defines max height in em units (6 em), which assuming default font size as 14 results in 84 px,
|
||||||
|
// which also seems to match observations upon inspecting the web element.
|
||||||
|
private const float max_height = 84f;
|
||||||
|
|
||||||
|
public CommentImageContainer(string url)
|
||||||
|
: base(url)
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Sprite CreateImageSprite() => new Sprite
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override Texture GetImageTexture(TextureStore textures, string url)
|
||||||
|
{
|
||||||
|
Texture t = base.GetImageTexture(textures, url);
|
||||||
|
|
||||||
|
if (t != null)
|
||||||
|
Size = t.Height > max_height ? new Vector2(max_height / t.Height * t.Width, max_height) : t.Size;
|
||||||
|
|
||||||
|
return t!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,12 +98,6 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (DistanceSpacingMultiplier.Disabled)
|
|
||||||
{
|
|
||||||
distanceSpacingSlider.Hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DistanceSpacingMultiplier.Value = editorBeatmap.BeatmapInfo.DistanceSpacing;
|
DistanceSpacingMultiplier.Value = editorBeatmap.BeatmapInfo.DistanceSpacing;
|
||||||
DistanceSpacingMultiplier.BindValueChanged(multiplier =>
|
DistanceSpacingMultiplier.BindValueChanged(multiplier =>
|
||||||
{
|
{
|
||||||
@ -116,6 +110,8 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
editorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
|
editorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
DistanceSpacingMultiplier.BindDisabledChanged(disabled => distanceSpacingSlider.Alpha = disabled ? 0 : 1, true);
|
||||||
|
|
||||||
// Manual binding to handle enabling distance spacing when the slider is interacted with.
|
// Manual binding to handle enabling distance spacing when the slider is interacted with.
|
||||||
distanceSpacingSlider.Current.BindValueChanged(spacing =>
|
distanceSpacingSlider.Current.BindValueChanged(spacing =>
|
||||||
{
|
{
|
||||||
|
@ -159,6 +159,26 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal bool IsInitialized;
|
internal bool IsInitialized;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The minimum allowable volume for sample playback.
|
||||||
|
/// <see cref="Samples"/> quieter than that will be forcibly played at this volume instead.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Drawable hitobjects adding their own custom samples, or other sample playback sources
|
||||||
|
/// (i.e. <see cref="GameplaySampleTriggerSource"/>) must enforce this themselves.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// This sample volume floor is present in stable, although it is set at 8% rather than 5%.
|
||||||
|
/// See: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Audio/AudioEngine.cs#L1070,
|
||||||
|
/// https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Audio/AudioEngine.cs#L1404-L1405.
|
||||||
|
/// The reason why it is 5% here is that the 8% cap was enforced in a silent manner
|
||||||
|
/// (i.e. the minimum selectable volume in the editor was 5%, but it would be played at 8% anyways),
|
||||||
|
/// which is confusing and arbitrary, so we're just doing 5% here at the cost of sacrificing strict parity.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public const int MINIMUM_SAMPLE_VOLUME = 5;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="DrawableHitObject"/>.
|
/// Creates a new <see cref="DrawableHitObject"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -181,7 +201,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
comboColourBrightness.BindTo(gameplaySettings.ComboColourNormalisationAmount);
|
comboColourBrightness.BindTo(gameplaySettings.ComboColourNormalisationAmount);
|
||||||
|
|
||||||
// Explicit non-virtual function call in case a DrawableHitObject overrides AddInternal.
|
// Explicit non-virtual function call in case a DrawableHitObject overrides AddInternal.
|
||||||
base.AddInternal(Samples = new PausableSkinnableSound());
|
base.AddInternal(Samples = new PausableSkinnableSound
|
||||||
|
{
|
||||||
|
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME
|
||||||
|
});
|
||||||
|
|
||||||
CurrentSkin = skinSource;
|
CurrentSkin = skinSource;
|
||||||
CurrentSkin.SourceChanged += skinSourceChanged;
|
CurrentSkin.SourceChanged += skinSourceChanged;
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -45,7 +46,10 @@ namespace osu.Game.Rulesets.UI
|
|||||||
Child = hitSounds = new Container<SkinnableSound>
|
Child = hitSounds = new Container<SkinnableSound>
|
||||||
{
|
{
|
||||||
Name = "concurrent sample pool",
|
Name = "concurrent sample pool",
|
||||||
ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound())
|
ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound
|
||||||
|
{
|
||||||
|
MinimumSampleVolume = DrawableHitObject.MINIMUM_SAMPLE_VOLUME
|
||||||
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ using osu.Game.Users;
|
|||||||
|
|
||||||
namespace osu.Game.Scoring
|
namespace osu.Game.Scoring
|
||||||
{
|
{
|
||||||
public interface IScoreInfo : IHasOnlineID<long>, IHasNamedFiles
|
public interface IScoreInfo : IHasOnlineID<long>
|
||||||
{
|
{
|
||||||
IUser User { get; }
|
IUser User { get; }
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ namespace osu.Game.Scoring
|
|||||||
|
|
||||||
double Accuracy { get; }
|
double Accuracy { get; }
|
||||||
|
|
||||||
bool HasReplay { get; }
|
long LegacyOnlineID { get; }
|
||||||
|
|
||||||
DateTimeOffset Date { get; }
|
DateTimeOffset Date { get; }
|
||||||
|
|
||||||
|
@ -19,6 +19,13 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
[JsonObject(MemberSerialization.OptIn)]
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
public class LegacyReplaySoloScoreInfo
|
public class LegacyReplaySoloScoreInfo
|
||||||
{
|
{
|
||||||
|
/// <remarks>
|
||||||
|
/// The value of this property should correspond to <see cref="ScoreInfo.OnlineID"/>
|
||||||
|
/// (i.e. come from the `solo_scores` ID scheme).
|
||||||
|
/// </remarks>
|
||||||
|
[JsonProperty("online_id")]
|
||||||
|
public long OnlineID { get; set; } = -1;
|
||||||
|
|
||||||
[JsonProperty("mods")]
|
[JsonProperty("mods")]
|
||||||
public APIMod[] Mods { get; set; } = Array.Empty<APIMod>();
|
public APIMod[] Mods { get; set; } = Array.Empty<APIMod>();
|
||||||
|
|
||||||
@ -30,6 +37,7 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
|
|
||||||
public static LegacyReplaySoloScoreInfo FromScore(ScoreInfo score) => new LegacyReplaySoloScoreInfo
|
public static LegacyReplaySoloScoreInfo FromScore(ScoreInfo score) => new LegacyReplaySoloScoreInfo
|
||||||
{
|
{
|
||||||
|
OnlineID = score.OnlineID,
|
||||||
Mods = score.APIMods,
|
Mods = score.APIMods,
|
||||||
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||||
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||||
|
@ -101,9 +101,9 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
byte[] compressedReplay = sr.ReadByteArray();
|
byte[] compressedReplay = sr.ReadByteArray();
|
||||||
|
|
||||||
if (version >= 20140721)
|
if (version >= 20140721)
|
||||||
scoreInfo.OnlineID = sr.ReadInt64();
|
scoreInfo.LegacyOnlineID = sr.ReadInt64();
|
||||||
else if (version >= 20121008)
|
else if (version >= 20121008)
|
||||||
scoreInfo.OnlineID = sr.ReadInt32();
|
scoreInfo.LegacyOnlineID = sr.ReadInt32();
|
||||||
|
|
||||||
byte[] compressedScoreInfo = null;
|
byte[] compressedScoreInfo = null;
|
||||||
|
|
||||||
@ -121,6 +121,7 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
|
|
||||||
Debug.Assert(readScore != null);
|
Debug.Assert(readScore != null);
|
||||||
|
|
||||||
|
score.ScoreInfo.OnlineID = readScore.OnlineID;
|
||||||
score.ScoreInfo.Statistics = readScore.Statistics;
|
score.ScoreInfo.Statistics = readScore.Statistics;
|
||||||
score.ScoreInfo.MaximumStatistics = readScore.MaximumStatistics;
|
score.ScoreInfo.MaximumStatistics = readScore.MaximumStatistics;
|
||||||
score.ScoreInfo.Mods = readScore.Mods.Select(m => m.ToMod(currentRuleset)).ToArray();
|
score.ScoreInfo.Mods = readScore.Mods.Select(m => m.ToMod(currentRuleset)).ToArray();
|
||||||
|
@ -84,7 +84,7 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
sw.Write(getHpGraphFormatted());
|
sw.Write(getHpGraphFormatted());
|
||||||
sw.Write(score.ScoreInfo.Date.DateTime);
|
sw.Write(score.ScoreInfo.Date.DateTime);
|
||||||
sw.WriteByteArray(createReplayData());
|
sw.WriteByteArray(createReplayData());
|
||||||
sw.Write((long)0);
|
sw.Write(score.ScoreInfo.LegacyOnlineID);
|
||||||
writeModSpecificData(score.ScoreInfo, sw);
|
writeModSpecificData(score.ScoreInfo, sw);
|
||||||
sw.WriteByteArray(createScoreInfoData());
|
sw.WriteByteArray(createScoreInfoData());
|
||||||
}
|
}
|
||||||
|
@ -94,15 +94,32 @@ namespace osu.Game.Scoring
|
|||||||
|
|
||||||
public double Accuracy { get; set; }
|
public double Accuracy { get; set; }
|
||||||
|
|
||||||
public bool HasReplay => !string.IsNullOrEmpty(Hash);
|
[Ignored]
|
||||||
|
public bool HasOnlineReplay { get; set; }
|
||||||
|
|
||||||
public DateTimeOffset Date { get; set; }
|
public DateTimeOffset Date { get; set; }
|
||||||
|
|
||||||
public double? PP { get; set; }
|
public double? PP { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The online ID of this score.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// In the osu-web database, this ID (if present) comes from the new <c>solo_scores</c> table.
|
||||||
|
/// </remarks>
|
||||||
[Indexed]
|
[Indexed]
|
||||||
public long OnlineID { get; set; } = -1;
|
public long OnlineID { get; set; } = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The legacy online ID of this score.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// In the osu-web database, this ID (if present) comes from the legacy <c>osu_scores_*_high</c> tables.
|
||||||
|
/// This ID is also stored to replays set on osu!stable.
|
||||||
|
/// </remarks>
|
||||||
|
[Indexed]
|
||||||
|
public long LegacyOnlineID { get; set; } = -1;
|
||||||
|
|
||||||
[MapTo("User")]
|
[MapTo("User")]
|
||||||
public RealmUser RealmUser { get; set; } = null!;
|
public RealmUser RealmUser { get; set; } = null!;
|
||||||
|
|
||||||
@ -168,7 +185,6 @@ namespace osu.Game.Scoring
|
|||||||
IRulesetInfo IScoreInfo.Ruleset => Ruleset;
|
IRulesetInfo IScoreInfo.Ruleset => Ruleset;
|
||||||
IBeatmapInfo? IScoreInfo.Beatmap => BeatmapInfo;
|
IBeatmapInfo? IScoreInfo.Beatmap => BeatmapInfo;
|
||||||
IUser IScoreInfo.User => User;
|
IUser IScoreInfo.User => User;
|
||||||
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
|
|
||||||
|
|
||||||
#region Properties required to make things work with existing usages
|
#region Properties required to make things work with existing usages
|
||||||
|
|
||||||
|
@ -150,7 +150,11 @@ namespace osu.Game.Scoring
|
|||||||
|
|
||||||
public Task Import(ImportTask[] imports, ImportParameters parameters = default) => scoreImporter.Import(imports, parameters);
|
public Task Import(ImportTask[] imports, ImportParameters parameters = default) => scoreImporter.Import(imports, parameters);
|
||||||
|
|
||||||
public override bool IsAvailableLocally(ScoreInfo model) => Realm.Run(realm => realm.All<ScoreInfo>().Any(s => s.OnlineID == model.OnlineID));
|
public override bool IsAvailableLocally(ScoreInfo model)
|
||||||
|
=> Realm.Run(realm => realm.All<ScoreInfo>()
|
||||||
|
// this basically inlines `ModelExtension.MatchesOnlineID(IScoreInfo, IScoreInfo)`,
|
||||||
|
// because that method can't be used here, as realm can't translate it to its query language.
|
||||||
|
.Any(s => s.OnlineID == model.OnlineID || s.LegacyOnlineID == model.LegacyOnlineID));
|
||||||
|
|
||||||
public IEnumerable<string> HandledExtensions => scoreImporter.HandledExtensions;
|
public IEnumerable<string> HandledExtensions => scoreImporter.HandledExtensions;
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ using osu.Game.Audio;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Screens.Edit.Timing;
|
using osu.Game.Screens.Edit.Timing;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -101,7 +102,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
},
|
},
|
||||||
volume = new IndeterminateSliderWithTextBoxInput<int>("Volume", new BindableInt(100)
|
volume = new IndeterminateSliderWithTextBoxInput<int>("Volume", new BindableInt(100)
|
||||||
{
|
{
|
||||||
MinValue = 0,
|
MinValue = DrawableHitObject.MINIMUM_SAMPLE_VOLUME,
|
||||||
MaxValue = 100,
|
MaxValue = 100,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -425,7 +425,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
{
|
{
|
||||||
if (HasUnsavedChanges)
|
if (HasUnsavedChanges)
|
||||||
{
|
{
|
||||||
dialogOverlay.Push(new SaveBeforeGameplayTestDialog(() =>
|
dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to test it.", () =>
|
||||||
{
|
{
|
||||||
if (!Save()) return;
|
if (!Save()) return;
|
||||||
|
|
||||||
@ -1018,25 +1018,36 @@ namespace osu.Game.Screens.Edit
|
|||||||
{
|
{
|
||||||
var exportItems = new List<MenuItem>
|
var exportItems = new List<MenuItem>
|
||||||
{
|
{
|
||||||
new EditorMenuItem(EditorStrings.ExportForEditing, MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
new EditorMenuItem(EditorStrings.ExportForEditing, MenuItemType.Standard, () => exportBeatmap(false)) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
||||||
new EditorMenuItem(EditorStrings.ExportForCompatibility, MenuItemType.Standard, exportLegacyBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
new EditorMenuItem(EditorStrings.ExportForCompatibility, MenuItemType.Standard, () => exportBeatmap(true)) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
||||||
};
|
};
|
||||||
|
|
||||||
return new EditorMenuItem(CommonStrings.Export) { Items = exportItems };
|
return new EditorMenuItem(CommonStrings.Export) { Items = exportItems };
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exportBeatmap()
|
private void exportBeatmap(bool legacy)
|
||||||
{
|
{
|
||||||
if (!Save()) return;
|
if (HasUnsavedChanges)
|
||||||
|
{
|
||||||
|
dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to export it.", () =>
|
||||||
|
{
|
||||||
|
if (!Save()) return;
|
||||||
|
|
||||||
beatmapManager.Export(Beatmap.Value.BeatmapSetInfo);
|
runExport();
|
||||||
}
|
}));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
runExport();
|
||||||
|
}
|
||||||
|
|
||||||
private void exportLegacyBeatmap()
|
void runExport()
|
||||||
{
|
{
|
||||||
if (!Save()) return;
|
if (legacy)
|
||||||
|
beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo);
|
||||||
beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo);
|
else
|
||||||
|
beatmapManager.Export(Beatmap.Value.BeatmapSetInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Game.Overlays.Dialog;
|
using osu.Game.Overlays.Dialog;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit.GameplayTest
|
namespace osu.Game.Screens.Edit
|
||||||
{
|
{
|
||||||
public partial class SaveBeforeGameplayTestDialog : PopupDialog
|
public partial class SaveRequiredPopupDialog : PopupDialog
|
||||||
{
|
{
|
||||||
public SaveBeforeGameplayTestDialog(Action saveAndPreview)
|
public SaveRequiredPopupDialog(string headerText, Action saveAndAction)
|
||||||
{
|
{
|
||||||
HeaderText = "The beatmap will be saved in order to test it.";
|
HeaderText = headerText;
|
||||||
|
|
||||||
Icon = FontAwesome.Regular.Save;
|
Icon = FontAwesome.Regular.Save;
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ namespace osu.Game.Screens.Edit.GameplayTest
|
|||||||
new PopupDialogOkButton
|
new PopupDialogOkButton
|
||||||
{
|
{
|
||||||
Text = "Sounds good, let's go!",
|
Text = "Sounds good, let's go!",
|
||||||
Action = saveAndPreview
|
Action = saveAndAction
|
||||||
},
|
},
|
||||||
new PopupDialogCancelButton
|
new PopupDialogCancelButton
|
||||||
{
|
{
|
@ -10,7 +10,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
public partial class MultiplayerResultsScreen : PlaylistsResultsScreen
|
public partial class MultiplayerResultsScreen : PlaylistsResultsScreen
|
||||||
{
|
{
|
||||||
public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem)
|
public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem)
|
||||||
: base(score, roomId, playlistItem, false, false)
|
: base(score, roomId, playlistItem, false)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
private readonly WorkingBeatmap beatmap;
|
private readonly WorkingBeatmap beatmap;
|
||||||
|
|
||||||
private readonly Track track;
|
private Track track;
|
||||||
|
|
||||||
private readonly double skipTargetTime;
|
private readonly double skipTargetTime;
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
protected override void StartGameplayClock()
|
protected override void StartGameplayClock()
|
||||||
{
|
{
|
||||||
addSourceClockAdjustments();
|
addAdjustmentsToTrack();
|
||||||
|
|
||||||
base.StartGameplayClock();
|
base.StartGameplayClock();
|
||||||
|
|
||||||
@ -186,20 +186,20 @@ namespace osu.Game.Screens.Play
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void StopUsingBeatmapClock()
|
public void StopUsingBeatmapClock()
|
||||||
{
|
{
|
||||||
removeSourceClockAdjustments();
|
removeAdjustmentsFromTrack();
|
||||||
|
|
||||||
var virtualTrack = new TrackVirtual(beatmap.Track.Length);
|
track = new TrackVirtual(beatmap.Track.Length);
|
||||||
virtualTrack.Seek(CurrentTime);
|
track.Seek(CurrentTime);
|
||||||
if (IsRunning)
|
if (IsRunning)
|
||||||
virtualTrack.Start();
|
track.Start();
|
||||||
ChangeSource(virtualTrack);
|
ChangeSource(track);
|
||||||
|
|
||||||
addSourceClockAdjustments();
|
addAdjustmentsToTrack();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool speedAdjustmentsApplied;
|
private bool speedAdjustmentsApplied;
|
||||||
|
|
||||||
private void addSourceClockAdjustments()
|
private void addAdjustmentsToTrack()
|
||||||
{
|
{
|
||||||
if (speedAdjustmentsApplied)
|
if (speedAdjustmentsApplied)
|
||||||
return;
|
return;
|
||||||
@ -213,7 +213,7 @@ namespace osu.Game.Screens.Play
|
|||||||
speedAdjustmentsApplied = true;
|
speedAdjustmentsApplied = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeSourceClockAdjustments()
|
private void removeAdjustmentsFromTrack()
|
||||||
{
|
{
|
||||||
if (!speedAdjustmentsApplied)
|
if (!speedAdjustmentsApplied)
|
||||||
return;
|
return;
|
||||||
@ -228,7 +228,7 @@ namespace osu.Game.Screens.Play
|
|||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
removeSourceClockAdjustments();
|
removeAdjustmentsFromTrack();
|
||||||
}
|
}
|
||||||
|
|
||||||
ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo;
|
ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo;
|
||||||
|
@ -1110,13 +1110,14 @@ namespace osu.Game.Screens.Play
|
|||||||
failAnimationContainer?.Stop();
|
failAnimationContainer?.Stop();
|
||||||
PauseOverlay?.StopAllSamples();
|
PauseOverlay?.StopAllSamples();
|
||||||
|
|
||||||
if (LoadedBeatmapSuccessfully)
|
if (LoadedBeatmapSuccessfully && !GameplayState.HasPassed)
|
||||||
{
|
{
|
||||||
if (!GameplayState.HasPassed && !GameplayState.HasFailed)
|
Debug.Assert(resultsDisplayDelegate == null);
|
||||||
|
|
||||||
|
if (!GameplayState.HasFailed)
|
||||||
GameplayState.HasQuit = true;
|
GameplayState.HasQuit = true;
|
||||||
|
|
||||||
// if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap.
|
if (DrawableRuleset.ReplayScore == null)
|
||||||
if (prepareScoreForDisplayTask == null && DrawableRuleset.ReplayScore == null)
|
|
||||||
ScoreProcessor.FailScore(Score.ScoreInfo);
|
ScoreProcessor.FailScore(Score.ScoreInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1166,13 +1167,6 @@ namespace osu.Game.Screens.Play
|
|||||||
// the import process will re-attach managed beatmap/rulesets to this score. we don't want this for now, so create a temporary copy to import.
|
// the import process will re-attach managed beatmap/rulesets to this score. we don't want this for now, so create a temporary copy to import.
|
||||||
var importableScore = score.ScoreInfo.DeepClone();
|
var importableScore = score.ScoreInfo.DeepClone();
|
||||||
|
|
||||||
// For the time being, online ID responses are not really useful for anything.
|
|
||||||
// In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores.
|
|
||||||
//
|
|
||||||
// Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint
|
|
||||||
// conflicts across various systems (ie. solo and multiplayer).
|
|
||||||
importableScore.OnlineID = -1;
|
|
||||||
|
|
||||||
var imported = scoreManager.Import(importableScore, replayReader);
|
var imported = scoreManager.Import(importableScore, replayReader);
|
||||||
|
|
||||||
imported.PerformRead(s =>
|
imported.PerformRead(s =>
|
||||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Screens.Ranking
|
|||||||
if (State.Value == DownloadState.LocallyAvailable)
|
if (State.Value == DownloadState.LocallyAvailable)
|
||||||
return ReplayAvailability.Local;
|
return ReplayAvailability.Local;
|
||||||
|
|
||||||
if (Score.Value?.HasReplay == true)
|
if (Score.Value?.HasOnlineReplay == true)
|
||||||
return ReplayAvailability.Online;
|
return ReplayAvailability.Online;
|
||||||
|
|
||||||
return ReplayAvailability.NotAvailable;
|
return ReplayAvailability.NotAvailable;
|
||||||
|
@ -7,6 +7,7 @@ using System.Linq;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Extensions;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Online.API.Requests;
|
||||||
using osu.Game.Online.Solo;
|
using osu.Game.Online.Solo;
|
||||||
@ -67,7 +68,7 @@ namespace osu.Game.Screens.Ranking
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset);
|
getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset);
|
||||||
getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineID != Score.OnlineID).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo)));
|
getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => !s.MatchesOnlineID(Score)).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo)));
|
||||||
return getScoreRequest;
|
return getScoreRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1197,6 +1197,8 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
private bool rightMouseScrollBlocked;
|
private bool rightMouseScrollBlocked;
|
||||||
|
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||||
|
|
||||||
public CarouselScrollContainer()
|
public CarouselScrollContainer()
|
||||||
{
|
{
|
||||||
// size is determined by the carousel itself, due to not all content necessarily being loaded.
|
// size is determined by the carousel itself, due to not all content necessarily being loaded.
|
||||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
|
criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
|
||||||
match &= !criteria.Title.HasFilter || criteria.Title.Matches(BeatmapInfo.Metadata.Title) ||
|
match &= !criteria.Title.HasFilter || criteria.Title.Matches(BeatmapInfo.Metadata.Title) ||
|
||||||
criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode);
|
criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode);
|
||||||
|
match &= !criteria.DifficultyName.HasFilter || criteria.DifficultyName.Matches(BeatmapInfo.DifficultyName);
|
||||||
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating);
|
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating);
|
||||||
|
|
||||||
if (!match) return false;
|
if (!match) return false;
|
||||||
|
@ -38,6 +38,7 @@ namespace osu.Game.Screens.Select
|
|||||||
public OptionalTextFilter Creator;
|
public OptionalTextFilter Creator;
|
||||||
public OptionalTextFilter Artist;
|
public OptionalTextFilter Artist;
|
||||||
public OptionalTextFilter Title;
|
public OptionalTextFilter Title;
|
||||||
|
public OptionalTextFilter DifficultyName;
|
||||||
|
|
||||||
public OptionalRange<double> UserStarDifficulty = new OptionalRange<double>
|
public OptionalRange<double> UserStarDifficulty = new OptionalRange<double>
|
||||||
{
|
{
|
||||||
@ -68,8 +69,23 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
string remainingText = value;
|
string remainingText = value;
|
||||||
|
|
||||||
|
// Match either an open difficulty tag to the end of string,
|
||||||
|
// or match a closed one with a whitespace after it.
|
||||||
|
//
|
||||||
|
// To keep things simple, the closing ']' may be included in the match group,
|
||||||
|
// and is trimmed post-match.
|
||||||
|
foreach (Match quotedSegment in Regex.Matches(value, "(^|\\s)\\[(.*)(\\]\\s|$)"))
|
||||||
|
{
|
||||||
|
DifficultyName = new OptionalTextFilter
|
||||||
|
{
|
||||||
|
SearchTerm = quotedSegment.Groups[2].Value.Trim(']')
|
||||||
|
};
|
||||||
|
|
||||||
|
remainingText = remainingText.Replace(quotedSegment.Value, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
// First handle quoted segments to ensure we keep inline spaces in exact matches.
|
// First handle quoted segments to ensure we keep inline spaces in exact matches.
|
||||||
foreach (Match quotedSegment in Regex.Matches(searchText, "(\"[^\"]+\"[!]?)"))
|
foreach (Match quotedSegment in Regex.Matches(value, "(\"[^\"]+\"[!]?)"))
|
||||||
{
|
{
|
||||||
terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value });
|
terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value });
|
||||||
remainingText = remainingText.Replace(quotedSegment.Value, string.Empty);
|
remainingText = remainingText.Replace(quotedSegment.Value, string.Empty);
|
||||||
|
@ -76,6 +76,9 @@ namespace osu.Game.Screens.Select
|
|||||||
case "title":
|
case "title":
|
||||||
return TryUpdateCriteriaText(ref criteria.Title, op, value);
|
return TryUpdateCriteriaText(ref criteria.Title, op, value);
|
||||||
|
|
||||||
|
case "diff":
|
||||||
|
return TryUpdateCriteriaText(ref criteria.DifficultyName, op, value);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false;
|
return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false;
|
||||||
}
|
}
|
||||||
|
@ -171,11 +171,6 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
AddRangeInternal(new Drawable[]
|
AddRangeInternal(new Drawable[]
|
||||||
{
|
{
|
||||||
new ResetScrollContainer(() => Carousel.ScrollToSelected())
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Y,
|
|
||||||
Width = 250,
|
|
||||||
},
|
|
||||||
new VerticalMaskingContainer
|
new VerticalMaskingContainer
|
||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
@ -243,6 +238,10 @@ namespace osu.Game.Screens.Select
|
|||||||
Padding = new MarginPadding { Top = left_area_padding },
|
Padding = new MarginPadding { Top = left_area_padding },
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
|
new LeftSideInteractionContainer(() => Carousel.ScrollToSelected())
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
beatmapInfoWedge = new BeatmapInfoWedge
|
beatmapInfoWedge = new BeatmapInfoWedge
|
||||||
{
|
{
|
||||||
Height = WEDGE_HEIGHT,
|
Height = WEDGE_HEIGHT,
|
||||||
@ -1017,18 +1016,28 @@ namespace osu.Game.Screens.Select
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private partial class ResetScrollContainer : Container
|
/// <summary>
|
||||||
|
/// Handles mouse interactions required when moving away from the carousel.
|
||||||
|
/// </summary>
|
||||||
|
internal partial class LeftSideInteractionContainer : Container
|
||||||
{
|
{
|
||||||
private readonly Action? onHoverAction;
|
private readonly Action? resetCarouselPosition;
|
||||||
|
|
||||||
public ResetScrollContainer(Action onHoverAction)
|
public LeftSideInteractionContainer(Action resetCarouselPosition)
|
||||||
{
|
{
|
||||||
this.onHoverAction = onHoverAction;
|
this.resetCarouselPosition = resetCarouselPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we want to block plain scrolls on the left side so that they don't scroll the carousel,
|
||||||
|
// but also we *don't* want to handle scrolls when they're combined with keyboard modifiers
|
||||||
|
// as those will usually correspond to other interactions like adjusting volume.
|
||||||
|
protected override bool OnScroll(ScrollEvent e) => !e.ControlPressed && !e.AltPressed && !e.ShiftPressed && !e.SuperPressed;
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
{
|
{
|
||||||
onHoverAction?.Invoke();
|
resetCarouselPosition?.Invoke();
|
||||||
return base.OnHover(e);
|
return base.OnHover(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ namespace osu.Game.Skinning
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.TopRight,
|
Anchor = Anchor.TopRight,
|
||||||
Origin = Anchor.TopRight,
|
Origin = Anchor.TopRight,
|
||||||
|
FixedWidth = true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ namespace osu.Game.Skinning
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.TopRight,
|
Anchor = Anchor.TopRight,
|
||||||
Origin = Anchor.TopRight,
|
Origin = Anchor.TopRight,
|
||||||
|
FixedWidth = true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,7 +200,11 @@ namespace osu.Game.Skinning
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const double default_frame_time = 1000 / 60d;
|
/// <summary>
|
||||||
|
/// The frame length of each frame at a 60 FPS rate.
|
||||||
|
/// Default frame rate for legacy skin animations.
|
||||||
|
/// </summary>
|
||||||
|
public const double SIXTY_FRAME_TIME = 1000 / 60d;
|
||||||
|
|
||||||
private static double getFrameLength(ISkin source, bool applyConfigFrameRate, Texture[] textures)
|
private static double getFrameLength(ISkin source, bool applyConfigFrameRate, Texture[] textures)
|
||||||
{
|
{
|
||||||
@ -214,7 +218,7 @@ namespace osu.Game.Skinning
|
|||||||
return 1000f / textures.Length;
|
return 1000f / textures.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
return default_frame_time;
|
return SIXTY_FRAME_TIME;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
@ -12,8 +13,10 @@ namespace osu.Game.Skinning
|
|||||||
{
|
{
|
||||||
public sealed partial class LegacySpriteText : OsuSpriteText
|
public sealed partial class LegacySpriteText : OsuSpriteText
|
||||||
{
|
{
|
||||||
|
public Vector2? MaxSizePerGlyph { get; init; }
|
||||||
|
public bool FixedWidth { get; init; }
|
||||||
|
|
||||||
private readonly LegacyFont font;
|
private readonly LegacyFont font;
|
||||||
private readonly Vector2? maxSizePerGlyph;
|
|
||||||
|
|
||||||
private LegacyGlyphStore glyphStore = null!;
|
private LegacyGlyphStore glyphStore = null!;
|
||||||
|
|
||||||
@ -21,10 +24,18 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' };
|
protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' };
|
||||||
|
|
||||||
public LegacySpriteText(LegacyFont font, Vector2? maxSizePerGlyph = null)
|
// ReSharper disable once UnusedMember.Global
|
||||||
|
// being unused is the point here
|
||||||
|
public new FontUsage Font
|
||||||
|
{
|
||||||
|
get => base.Font;
|
||||||
|
set => throw new InvalidOperationException(@"Attempting to use this setter will not work correctly. "
|
||||||
|
+ $@"Use specific init-only properties exposed by {nameof(LegacySpriteText)} instead.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public LegacySpriteText(LegacyFont font)
|
||||||
{
|
{
|
||||||
this.font = font;
|
this.font = font;
|
||||||
this.maxSizePerGlyph = maxSizePerGlyph;
|
|
||||||
|
|
||||||
Shadow = false;
|
Shadow = false;
|
||||||
UseFullGlyphHeight = false;
|
UseFullGlyphHeight = false;
|
||||||
@ -33,10 +44,10 @@ namespace osu.Game.Skinning
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ISkinSource skin)
|
private void load(ISkinSource skin)
|
||||||
{
|
{
|
||||||
Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true);
|
base.Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: FixedWidth);
|
||||||
Spacing = new Vector2(-skin.GetFontOverlap(font), 0);
|
Spacing = new Vector2(-skin.GetFontOverlap(font), 0);
|
||||||
|
|
||||||
glyphStore = new LegacyGlyphStore(skin, maxSizePerGlyph);
|
glyphStore = new LegacyGlyphStore(skin, MaxSizePerGlyph);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore);
|
protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore);
|
||||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Skinning
|
|||||||
{
|
{
|
||||||
[MapTo("Skin")]
|
[MapTo("Skin")]
|
||||||
[JsonObject(MemberSerialization.OptIn)]
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable<SkinInfo>, IHasGuidPrimaryKey, ISoftDelete, IHasNamedFiles
|
public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable<SkinInfo>, IHasGuidPrimaryKey, ISoftDelete
|
||||||
{
|
{
|
||||||
internal static readonly Guid TRIANGLES_SKIN = new Guid("2991CFD8-2140-469A-BCB9-2EC23FBCE4AD");
|
internal static readonly Guid TRIANGLES_SKIN = new Guid("2991CFD8-2140-469A-BCB9-2EC23FBCE4AD");
|
||||||
internal static readonly Guid ARGON_SKIN = new Guid("CFFA69DE-B3E3-4DEE-8563-3C4F425C05D0");
|
internal static readonly Guid ARGON_SKIN = new Guid("CFFA69DE-B3E3-4DEE-8563-3C4F425C05D0");
|
||||||
|
@ -182,7 +182,10 @@ namespace osu.Game.Skinning
|
|||||||
Name = NamingUtils.GetNextBestName(existingSkinNames, $@"{s.Name} (modified)")
|
Name = NamingUtils.GetNextBestName(existingSkinNames, $@"{s.Name} (modified)")
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = skinImporter.ImportModel(skinInfo);
|
var result = skinImporter.ImportModel(skinInfo, parameters: new ImportParameters
|
||||||
|
{
|
||||||
|
ImportImmediately = true // to avoid possible deadlocks when editing skin during gameplay.
|
||||||
|
});
|
||||||
|
|
||||||
if (result != null)
|
if (result != null)
|
||||||
{
|
{
|
||||||
|
@ -20,6 +20,12 @@ namespace osu.Game.Skinning
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent
|
public partial class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The minimum allowable volume for <see cref="Samples"/>.
|
||||||
|
/// <see cref="Samples"/> that specify a lower <see cref="ISampleInfo.Volume"/> will be forcibly pulled up to this volume.
|
||||||
|
/// </summary>
|
||||||
|
public int MinimumSampleVolume { get; set; }
|
||||||
|
|
||||||
public override bool RemoveWhenNotAlive => false;
|
public override bool RemoveWhenNotAlive => false;
|
||||||
public override bool RemoveCompletedTransforms => false;
|
public override bool RemoveCompletedTransforms => false;
|
||||||
|
|
||||||
@ -156,7 +162,7 @@ namespace osu.Game.Skinning
|
|||||||
{
|
{
|
||||||
var sample = samplePool?.GetPooledSample(s) ?? new PoolableSkinnableSample(s);
|
var sample = samplePool?.GetPooledSample(s) ?? new PoolableSkinnableSample(s);
|
||||||
sample.Looping = Looping;
|
sample.Looping = Looping;
|
||||||
sample.Volume.Value = s.Volume / 100.0;
|
sample.Volume.Value = Math.Max(s.Volume, MinimumSampleVolume) / 100.0;
|
||||||
|
|
||||||
samplesContainer.Add(sample);
|
samplesContainer.Add(sample);
|
||||||
}
|
}
|
||||||
|
@ -30,8 +30,12 @@ namespace osu.Game.Storyboards
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This iterates all elements and as such should be used sparingly or stored locally.
|
/// This iterates all elements and as such should be used sparingly or stored locally.
|
||||||
|
/// Sample events use their start time as "end time" during this calculation.
|
||||||
|
/// Video and background events are not included to match stable.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public double? EarliestEventTime => Layers.SelectMany(l => l.Elements).MinBy(e => e.StartTime)?.StartTime;
|
public double? EarliestEventTime => Layers.SelectMany(l => l.Elements)
|
||||||
|
.Where(e => e is not StoryboardVideo)
|
||||||
|
.MinBy(e => e.StartTime)?.StartTime;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Across all layers, find the latest point in time that a storyboard element ends at.
|
/// Across all layers, find the latest point in time that a storyboard element ends at.
|
||||||
@ -39,9 +43,12 @@ namespace osu.Game.Storyboards
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This iterates all elements and as such should be used sparingly or stored locally.
|
/// This iterates all elements and as such should be used sparingly or stored locally.
|
||||||
/// Videos and samples return StartTime as their EndTIme.
|
/// Sample events use their start time as "end time" during this calculation.
|
||||||
|
/// Video and background events are not included to match stable.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public double? LatestEventTime => Layers.SelectMany(l => l.Elements).MaxBy(e => e.GetEndTime())?.GetEndTime();
|
public double? LatestEventTime => Layers.SelectMany(l => l.Elements)
|
||||||
|
.Where(e => e is not StoryboardVideo)
|
||||||
|
.MaxBy(e => e.GetEndTime())?.GetEndTime();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Depth of the currently front-most storyboard layer, excluding the overlay layer.
|
/// Depth of the currently front-most storyboard layer, excluding the overlay layer.
|
||||||
|
@ -432,6 +432,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
private bool running;
|
private bool running;
|
||||||
|
|
||||||
|
public override double Rate => base.Rate
|
||||||
|
// This is mainly to allow some tests to override the rate to zero
|
||||||
|
// and avoid interpolation.
|
||||||
|
* referenceClock.Rate;
|
||||||
|
|
||||||
public TrackVirtualManual(IFrameBasedClock referenceClock, string name = "virtual")
|
public TrackVirtualManual(IFrameBasedClock referenceClock, string name = "virtual")
|
||||||
: base(name)
|
: base(name)
|
||||||
{
|
{
|
||||||
|
@ -37,9 +37,10 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="11.5.0" />
|
<PackageReference Include="Realm" Version="11.5.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2023.1012.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2023.1012.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1014.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1023.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.40.0" />
|
<PackageReference Include="Sentry" Version="3.40.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.34.1" />
|
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||||
|
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.6" />
|
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.6" />
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||||
|