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
|
||||
# install (or update) templates package.
|
||||
# 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
|
||||
dotnet new ruleset -n MyCoolRuleset
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
@ -179,5 +180,33 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
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
|
||||
},
|
||||
tailContainer = new Container<DrawableHoldNoteTail> { RelativeSizeAxes = Axes.Both },
|
||||
slidingSample = new PausableSkinnableSound { Looping = true }
|
||||
slidingSample = new PausableSkinnableSound
|
||||
{
|
||||
Looping = true,
|
||||
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
|
||||
}
|
||||
});
|
||||
|
||||
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]
|
||||
Version: latest
|
||||
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,
|
||||
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 Medium Stream", () => SetContents(_ => testStream(5, true)));
|
||||
AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true)));
|
||||
AddStep("High combo index", () => SetContents(_ => testSingle(2, true, comboIndex: 15)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -66,12 +67,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
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();
|
||||
|
||||
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;
|
||||
}
|
||||
@ -84,14 +85,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@ -99,6 +100,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = Time.Current + 1000 + timeOffset,
|
||||
Position = OsuPlayfield.BASE_SIZE / 4 + positionOffset.Value,
|
||||
IndexInCurrentCombo = comboIndex,
|
||||
};
|
||||
|
||||
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>();
|
||||
|
||||
[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.
|
||||
// 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.
|
||||
@ -76,6 +112,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
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.
|
||||
// If not, hitsounds will not play on time.
|
||||
AddAssert("Judgement offset is zero", () => judgementResults.Last().TimeOffset == 0);
|
||||
@ -121,6 +159,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
else
|
||||
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.
|
||||
// If not, hitsounds will not play on time.
|
||||
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.
|
||||
/// </summary>
|
||||
[Test]
|
||||
[Ignore("An upcoming implementation will fix this case")]
|
||||
public void TestVibrateWithoutSpinningOffCentre()
|
||||
{
|
||||
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.
|
||||
/// </summary>
|
||||
[Test]
|
||||
[Ignore("An upcoming implementation will fix this case")]
|
||||
public void TestVibrateWithoutSpinningOnCentre()
|
||||
{
|
||||
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).
|
||||
/// </summary>
|
||||
[Test]
|
||||
[Ignore("An upcoming implementation will fix this case")]
|
||||
public void TestSpinHalfBothDirections()
|
||||
{
|
||||
performTest(new SpinFramesGenerator(time_spinner_start)
|
||||
@ -149,7 +146,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[TestCase(-180, 540, 1)]
|
||||
[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)
|
||||
{
|
||||
performTest(new SpinFramesGenerator(time_spinner_start)
|
||||
@ -162,18 +158,28 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("An upcoming implementation will fix this case")]
|
||||
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)
|
||||
.Spin(360, 500) // 2000ms -> 1 full CW spin
|
||||
.Spin(-180, 500) // 2500ms -> 0.5 CCW spins
|
||||
.Spin(90, 500) // 3000ms -> 0.25 CW spins
|
||||
.Spin(450, 500) // 3500ms -> 1 full CW spin
|
||||
.Spin(180, 500) // 4000ms -> 0.5 CW spins
|
||||
.Build();
|
||||
List<ReplayFrame> frames =
|
||||
new SpinFramesGenerator(time_spinner_start)
|
||||
// 1500ms start
|
||||
.Spin(360, 500)
|
||||
// 2000ms -> 1 full CW spin
|
||||
.Spin(-180, 500)
|
||||
// 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);
|
||||
|
||||
@ -190,15 +196,35 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
DrawableSpinner drawableSpinner = null!;
|
||||
AddUntilStep("get spinner", () => (drawableSpinner = currentPlayer.ChildrenOfType<DrawableSpinner>().Single()) != null);
|
||||
|
||||
assertTotalRotation(4000, 900);
|
||||
assertFinalRotationCorrect();
|
||||
assertTotalRotation(3750, 810);
|
||||
assertTotalRotation(3500, 720);
|
||||
assertTotalRotation(3250, 530);
|
||||
assertTotalRotation(3000, 540);
|
||||
assertTotalRotation(3000, 450);
|
||||
assertTotalRotation(2750, 540);
|
||||
assertTotalRotation(2500, 540);
|
||||
assertTotalRotation(2250, 360);
|
||||
assertTotalRotation(2000, 180);
|
||||
assertTotalRotation(2250, 450);
|
||||
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);
|
||||
|
||||
void assertTotalRotation(double time, float expected)
|
||||
@ -211,8 +237,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
void addSeekStep(double 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));
|
||||
}
|
||||
|
||||
void assertFinalRotationCorrect() => assertTotalRotation(4000, 900);
|
||||
}
|
||||
|
||||
private void assertTicksHit(int count)
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Judgements
|
||||
{
|
||||
@ -15,28 +16,15 @@ namespace osu.Game.Rulesets.Osu.Judgements
|
||||
public Spinner Spinner => (Spinner)HitObject;
|
||||
|
||||
/// <summary>
|
||||
/// The total rotation performed on the spinner disc, disregarding the spin direction,
|
||||
/// adjusted for the track's playback rate.
|
||||
/// The total amount that the spinner was rotated.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This value is always non-negative and is monotonically increasing with time
|
||||
/// (i.e. will only increase if time is passing forward, but can decrease during rewind).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The rotation from each frame is multiplied by the clock's current playback rate.
|
||||
/// 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;
|
||||
public float TotalRotation => History.TotalRotation;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the spinning history of the spinner.<br />
|
||||
/// Instants of movement deltas may be added or removed from this in order to calculate the total rotation for the spinner.
|
||||
/// </summary>
|
||||
public readonly SpinnerSpinHistory History = new SpinnerSpinHistory();
|
||||
|
||||
/// <summary>
|
||||
/// 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.";
|
||||
|
||||
//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;
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override LocalisableString Description => "Everything rotates. EVERYTHING.";
|
||||
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;
|
||||
|
||||
|
@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
HeadCircle,
|
||||
TailCircle,
|
||||
repeatContainer,
|
||||
Body,
|
||||
};
|
||||
|
||||
@ -107,7 +108,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
|
||||
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
|
||||
Ball,
|
||||
slidingSample = new PausableSkinnableSound { Looping = true }
|
||||
slidingSample = new PausableSkinnableSound
|
||||
{
|
||||
Looping = true,
|
||||
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
|
||||
}
|
||||
});
|
||||
|
||||
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
||||
|
@ -4,7 +4,6 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Diagnostics;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -15,9 +14,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public new SliderHeadCircle HitObject => (SliderHeadCircle)base.HitObject;
|
||||
|
||||
[CanBeNull]
|
||||
public Slider Slider => DrawableSlider?.HitObject;
|
||||
|
||||
public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
||||
|
||||
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
|
||||
|
@ -17,7 +17,7 @@ using osuTK;
|
||||
|
||||
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;
|
||||
|
||||
@ -36,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
public bool Tracking { get; set; }
|
||||
|
||||
public DrawableSliderRepeat()
|
||||
: base(null)
|
||||
{
|
||||
@ -85,8 +87,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
if (HitObject.StartTime <= Time.Current)
|
||||
ApplyResult(r => r.Type = DrawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
// shared implementation with DrawableSliderTick.
|
||||
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()
|
||||
|
@ -4,6 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@ -129,16 +130,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
if (userTriggered)
|
||||
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
|
||||
// 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;
|
||||
|
||||
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.
|
||||
// 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);
|
||||
else if (timeOffset > 0)
|
||||
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)
|
||||
{
|
||||
// shared implementation with DrawableSliderRepeat.
|
||||
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()
|
||||
|
@ -106,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
spinningSample = new PausableSkinnableSound
|
||||
{
|
||||
Volume = { Value = 0 },
|
||||
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
|
||||
Looping = true,
|
||||
Frequency = { Value = spinning_sample_initial_frequency }
|
||||
}
|
||||
|
@ -25,6 +25,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
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>
|
||||
/// Apply a judgement result.
|
||||
/// </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;
|
||||
}
|
||||
|
||||
currentRotation += delta;
|
||||
|
||||
double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate;
|
||||
delta = (float)(delta * Math.Abs(rate));
|
||||
|
||||
Debug.Assert(Math.Abs(delta) <= 180);
|
||||
|
||||
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
|
||||
// (see: ModTimeRamp)
|
||||
drawableSpinner.Result.TotalRotation += (float)(Math.Abs(delta) * rate);
|
||||
currentRotation += delta;
|
||||
drawableSpinner.Result.History.ReportDelta(Time.Current, delta);
|
||||
}
|
||||
|
||||
private void resetState(DrawableHitObject obj)
|
||||
|
@ -1,35 +1,39 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacySliderBall : CompositeDrawable
|
||||
{
|
||||
private readonly Drawable animationContent;
|
||||
|
||||
private readonly ISkin skin;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private DrawableHitObject? parentObject { get; set; }
|
||||
|
||||
public Color4 BallColour => animationContent.Colour;
|
||||
|
||||
private Sprite layerNd = 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;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
@ -38,30 +42,39 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
[BackgroundDependencyLoader]
|
||||
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
|
||||
{
|
||||
Anchor = 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),
|
||||
},
|
||||
LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d =>
|
||||
ballAnimation = new LegacySkinExtensions.SkinnableTextureAnimation
|
||||
{
|
||||
d.Anchor = Anchor.Centre;
|
||||
d.Origin = Anchor.Centre;
|
||||
}), ballColour),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Colour = ballColour,
|
||||
},
|
||||
layerSpec = new Sprite
|
||||
{
|
||||
Anchor = 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,
|
||||
},
|
||||
};
|
||||
|
||||
if (parentObject != null)
|
||||
parentObject.HitObjectApplied += onHitObjectApplied;
|
||||
|
||||
onHitObjectApplied(parentObject);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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 _)
|
||||
{
|
||||
// Gets called by slider ticks, tails, etc., leading to duplicated
|
||||
@ -114,7 +147,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (parentObject != null)
|
||||
{
|
||||
parentObject.HitObjectApplied -= onHitObjectApplied;
|
||||
parentObject.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(SPRITE_SCALE),
|
||||
Y = SPINNER_TOP_OFFSET + 299,
|
||||
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
|
||||
},
|
||||
spmBackground = new Sprite
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
Origin = Anchor.TopRight,
|
||||
Scale = new Vector2(SPRITE_SCALE * 0.9f),
|
||||
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;
|
||||
|
||||
case OsuSkinComponents.SliderBall:
|
||||
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "", maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE);
|
||||
|
||||
// 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);
|
||||
if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null)
|
||||
return new LegacySliderBall(this);
|
||||
|
||||
return null;
|
||||
|
||||
@ -150,10 +145,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
return null;
|
||||
|
||||
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
|
||||
Scale = new Vector2(hitcircle_text_scale),
|
||||
MaxSizePerGlyph = OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale,
|
||||
};
|
||||
|
||||
case OsuSkinComponents.SpinnerBody:
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@ -14,53 +15,54 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonBarLine : CompositeDrawable
|
||||
{
|
||||
private Container majorEdgeContainer = null!;
|
||||
|
||||
private Bindable<bool> major = null!;
|
||||
|
||||
private Box mainLine = null!;
|
||||
private Drawable topAnchor = null!;
|
||||
private Drawable bottomAnchor = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject drawableHitObject)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
const float line_offset = 8;
|
||||
var majorPieceSize = new Vector2(6, 20);
|
||||
// Avoid flickering due to no anti-aliasing of boxes by default.
|
||||
var edgeSmoothness = new Vector2(0.3f);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
AddInternal(mainLine = new Box
|
||||
{
|
||||
line = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
EdgeSmoothness = new Vector2(0.5f, 0),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
majorEdgeContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
new Circle
|
||||
{
|
||||
Name = "Top line",
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Size = majorPieceSize,
|
||||
Y = -line_offset,
|
||||
},
|
||||
new Circle
|
||||
{
|
||||
Name = "Bottom line",
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Size = majorPieceSize,
|
||||
Y = line_offset,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
Name = "Bar line",
|
||||
EdgeSmoothness = edgeSmoothness,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
});
|
||||
|
||||
const float major_extension = 10;
|
||||
|
||||
AddInternal(topAnchor = new Box
|
||||
{
|
||||
Name = "Top anchor",
|
||||
EdgeSmoothness = edgeSmoothness,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Height = major_extension,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Colour = ColourInfo.GradientVertical(Colour4.Transparent, Colour4.White),
|
||||
});
|
||||
|
||||
AddInternal(bottomAnchor = new Box
|
||||
{
|
||||
Name = "Bottom anchor",
|
||||
EdgeSmoothness = edgeSmoothness,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Height = major_extension,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Colour = ColourInfo.GradientVertical(Colour4.White, Colour4.Transparent),
|
||||
});
|
||||
|
||||
major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy();
|
||||
}
|
||||
@ -71,13 +73,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
major.BindValueChanged(updateMajor, true);
|
||||
}
|
||||
|
||||
private Box line = null!;
|
||||
|
||||
private void updateMajor(ValueChangedEvent<bool> major)
|
||||
{
|
||||
line.Alpha = major.NewValue ? 1f : 0.5f;
|
||||
line.Width = major.NewValue ? 1 : 0.5f;
|
||||
majorEdgeContainer.Alpha = major.NewValue ? 1 : 0;
|
||||
mainLine.Alpha = major.NewValue ? 1f : 0.5f;
|
||||
topAnchor.Alpha = bottomAnchor.Alpha = major.NewValue ? mainLine.Alpha * 0.3f : 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(6, 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));
|
||||
}
|
||||
}
|
||||
|
||||
[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));
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
localConfig?.Dispose();
|
||||
|
@ -2,10 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Tests.NonVisual.Filtering
|
||||
@ -382,6 +384,57 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
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
|
||||
{
|
||||
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.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveBeforeGameplayTestDialog);
|
||||
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
|
||||
|
||||
AddStep("dismiss prompt", () =>
|
||||
{
|
||||
@ -165,7 +165,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
InputManager.MoveMouseTo(button);
|
||||
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());
|
||||
|
||||
|
@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
OnlineID = hasOnlineId ? online_score_id : 0,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
BeatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(),
|
||||
Hash = replayAvailable ? "online" : string.Empty,
|
||||
HasOnlineReplay = replayAvailable,
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 39828,
|
||||
|
@ -7,6 +7,7 @@ using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -16,6 +17,7 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
@ -34,6 +36,7 @@ using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Screens.Select.Options;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
@ -165,6 +168,41 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
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>
|
||||
/// 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).
|
||||
@ -209,6 +247,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddStep("end spectator before retry", () => Game.SpectatorClient.EndPlaying(player.GameplayState));
|
||||
|
||||
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("get new player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
|
||||
@ -221,6 +260,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
var getOriginalPlayer = playToCompletion();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
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.Play;
|
||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@ -31,7 +33,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
private SkinEditor skinEditor => Game.ChildrenOfType<SkinEditor>().FirstOrDefault();
|
||||
|
||||
[Test]
|
||||
public void TestEditComponentDuringGameplay()
|
||||
public void TestEditComponentFromGameplayScene()
|
||||
{
|
||||
advanceToSongSelect();
|
||||
openSkinEditor();
|
||||
@ -69,6 +71,28 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
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]
|
||||
public void TestComponentsDeselectedOnSkinEditorHide()
|
||||
{
|
||||
|
@ -64,7 +64,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
new[] { "Plain", "This is plain comment" },
|
||||
new[] { "Pinned", "This is pinned comment" },
|
||||
new[] { "Link", "Please visit https://osu.ppy.sh" },
|
||||
|
||||
new[] { "Big Image", "![](Backgrounds/bg1)" },
|
||||
new[] { "Small Image", "![](Cursor/cursortrail)" },
|
||||
new[]
|
||||
{
|
||||
"Heading", @"# Heading 1
|
||||
|
@ -362,7 +362,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo();
|
||||
score.TotalScore += 10 - i;
|
||||
score.Hash = $"test{i}";
|
||||
score.HasOnlineReplay = true;
|
||||
scores.Add(score);
|
||||
}
|
||||
|
||||
|
@ -18,26 +18,24 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
[Cached]
|
||||
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!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
Child = slider = new RoundedSliderBar<double>
|
||||
AddStep("create slider", () => Child = slider = new RoundedSliderBar<double>
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Current = current,
|
||||
Current = new BindableDouble(5)
|
||||
{
|
||||
Precision = 0.1,
|
||||
MinValue = 0,
|
||||
MaxValue = 15
|
||||
},
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.4f
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -55,5 +53,22 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
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]
|
||||
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!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
Child = slider = new ShearedSliderBar<double>
|
||||
AddStep("create slider", () => Child = slider = new ShearedSliderBar<double>
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Current = current,
|
||||
Current = new BindableDouble(5)
|
||||
{
|
||||
Precision = 0.1,
|
||||
MinValue = 0,
|
||||
MaxValue = 15
|
||||
},
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.4f
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -55,5 +53,22 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
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.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
@ -10,7 +11,7 @@ namespace osu.Game.Tournament.Components
|
||||
{
|
||||
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
|
||||
{
|
||||
@ -23,13 +24,13 @@ namespace osu.Game.Tournament.Components
|
||||
base.Current = new Bindable<string>(string.Empty);
|
||||
|
||||
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, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
current.Value = DateTimeOffset.Parse(sender.Text);
|
||||
current.Value = DateTimeOffset.Parse(sender.Text, DateTimeFormatInfo.InvariantInfo);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tournament
|
||||
@ -519,9 +518,6 @@ namespace osu.Game.Tournament
|
||||
case CountryCode.KE:
|
||||
return "KEN";
|
||||
|
||||
case CountryCode.SS:
|
||||
return "SSD";
|
||||
|
||||
case CountryCode.SR:
|
||||
return "SUR";
|
||||
|
||||
@ -763,7 +759,7 @@ namespace osu.Game.Tournament
|
||||
return "MOZ";
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(country));
|
||||
return country.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,13 +10,15 @@ namespace osu.Game.Database
|
||||
/// <summary>
|
||||
/// A model that contains a list of files it is responsible for.
|
||||
/// </summary>
|
||||
public interface IHasRealmFiles
|
||||
public interface IHasRealmFiles : IHasNamedFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
IList<RealmNamedFileUsage> Files { get; }
|
||||
new IList<RealmNamedFileUsage> Files { get; }
|
||||
|
||||
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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.
|
||||
/// 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.
|
||||
/// 36 2023-10-26 Add LegacyOnlineID to ScoreInfo. Move osu_scores_*_high IDs stored in OnlineID to LegacyOnlineID. Reset anomalous OnlineIDs.
|
||||
/// </summary>
|
||||
private const int schema_version = 35;
|
||||
private const int schema_version = 36;
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
|
||||
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");
|
||||
|
@ -261,7 +261,7 @@ namespace osu.Game.Database
|
||||
/// <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 =>
|
||||
{
|
||||
pauseIfNecessary(cancellationToken);
|
||||
pauseIfNecessary(parameters, cancellationToken);
|
||||
|
||||
TModel? existing;
|
||||
|
||||
@ -560,9 +560,9 @@ namespace osu.Game.Database
|
||||
/// <returns>Whether to perform deletion.</returns>
|
||||
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;
|
||||
|
||||
Logger.Log($@"{GetType().Name} is being paused.");
|
||||
|
@ -114,8 +114,24 @@ namespace osu.Game.Extensions
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance to compare.</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>
|
||||
public static bool MatchesOnlineID(this IScoreInfo? instance, IScoreInfo? other) => matchesOnlineID(instance, other);
|
||||
/// <returns>
|
||||
/// 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)
|
||||
{
|
||||
|
@ -98,7 +98,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativePositionAxes = Axes.X,
|
||||
Current = { Value = true },
|
||||
OnDoubleClicked = () => Current.SetDefault(),
|
||||
OnDoubleClicked = () =>
|
||||
{
|
||||
if (!Current.Disabled)
|
||||
Current.SetDefault();
|
||||
},
|
||||
},
|
||||
},
|
||||
hoverClickSounds = new HoverClickSounds()
|
||||
|
@ -101,7 +101,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativePositionAxes = Axes.X,
|
||||
Current = { Value = true },
|
||||
OnDoubleClicked = () => Current.SetDefault(),
|
||||
OnDoubleClicked = () =>
|
||||
{
|
||||
if (!Current.Disabled)
|
||||
Current.SetDefault();
|
||||
},
|
||||
},
|
||||
},
|
||||
hoverClickSounds = new HoverClickSounds()
|
||||
|
@ -149,6 +149,7 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
|
||||
new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene),
|
||||
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.F3 }, GlobalAction.DecreaseScrollSpeed),
|
||||
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed),
|
||||
|
@ -7,16 +7,16 @@ using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Online.API.Requests.Responses
|
||||
{
|
||||
[Serializable]
|
||||
public class SoloScoreInfo : IHasOnlineID<long>
|
||||
public class SoloScoreInfo : IScoreInfo
|
||||
{
|
||||
[JsonProperty("beatmap_id")]
|
||||
public int BeatmapID { get; set; }
|
||||
@ -138,6 +138,18 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
|
||||
#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}";
|
||||
|
||||
/// <summary>
|
||||
@ -178,6 +190,7 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
var score = new ScoreInfo
|
||||
{
|
||||
OnlineID = OnlineID,
|
||||
LegacyOnlineID = (long?)LegacyScoreId ?? -1,
|
||||
User = User ?? new APIUser { Id = UserID },
|
||||
BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
|
||||
Ruleset = new RulesetInfo { OnlineID = RulesetID },
|
||||
@ -189,7 +202,7 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
Statistics = Statistics,
|
||||
MaximumStatistics = MaximumStatistics,
|
||||
Date = EndedAt,
|
||||
Hash = HasReplay ? "online" : string.Empty, // TODO: temporary?
|
||||
HasOnlineReplay = HasReplay,
|
||||
Mods = mods,
|
||||
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),
|
||||
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")]
|
||||
public int? Position { get; set; }
|
||||
|
||||
[JsonProperty("has_replay")]
|
||||
public bool HasReplay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Any scores in the room around this score.
|
||||
/// </summary>
|
||||
@ -84,7 +87,7 @@ namespace osu.Game.Online.Rooms
|
||||
User = User,
|
||||
Accuracy = Accuracy,
|
||||
Date = EndedAt,
|
||||
Hash = string.Empty, // todo: temporary?
|
||||
HasOnlineReplay = HasReplay,
|
||||
Rank = Rank,
|
||||
Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty<Mod>(),
|
||||
Position = Position,
|
||||
|
@ -39,7 +39,8 @@ namespace osu.Game.Online
|
||||
var scoreInfo = new ScoreInfo
|
||||
{
|
||||
ID = TrackedItem.ID,
|
||||
OnlineID = TrackedItem.OnlineID
|
||||
OnlineID = TrackedItem.OnlineID,
|
||||
LegacyOnlineID = TrackedItem.LegacyOnlineID
|
||||
};
|
||||
|
||||
Downloader.DownloadBegan += downloadBegan;
|
||||
@ -47,6 +48,7 @@ namespace osu.Game.Online
|
||||
|
||||
realmSubscription = realm.RegisterForNotifications(r => r.All<ScoreInfo>().Where(s =>
|
||||
((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID)
|
||||
|| (s.LegacyOnlineID > 0 && s.LegacyOnlineID == TrackedItem.LegacyOnlineID)
|
||||
|| (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash))
|
||||
&& !s.DeletePending), (items, _) =>
|
||||
{
|
||||
|
@ -678,6 +678,9 @@ namespace osu.Game
|
||||
if (score.OnlineID > 0)
|
||||
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)
|
||||
databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == scoreInfo.Hash);
|
||||
|
||||
|
@ -2,8 +2,14 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
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.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Graphics.Containers.Markdown;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
@ -16,6 +22,8 @@ namespace osu.Game.Overlays.Comments
|
||||
|
||||
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock);
|
||||
|
||||
public override MarkdownTextFlowContainer CreateTextFlow() => new CommentMarkdownTextFlowContainer();
|
||||
|
||||
private partial class CommentMarkdownHeading : OsuMarkdownHeading
|
||||
{
|
||||
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.BindValueChanged(multiplier =>
|
||||
{
|
||||
@ -116,6 +110,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
editorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
|
||||
}, true);
|
||||
|
||||
DistanceSpacingMultiplier.BindDisabledChanged(disabled => distanceSpacingSlider.Alpha = disabled ? 0 : 1, true);
|
||||
|
||||
// Manual binding to handle enabling distance spacing when the slider is interacted with.
|
||||
distanceSpacingSlider.Current.BindValueChanged(spacing =>
|
||||
{
|
||||
|
@ -159,6 +159,26 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// </summary>
|
||||
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>
|
||||
/// Creates a new <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
@ -181,7 +201,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
comboColourBrightness.BindTo(gameplaySettings.ComboColourNormalisationAmount);
|
||||
|
||||
// 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.SourceChanged += skinSourceChanged;
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
@ -45,7 +46,10 @@ namespace osu.Game.Rulesets.UI
|
||||
Child = hitSounds = new Container<SkinnableSound>
|
||||
{
|
||||
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
|
||||
{
|
||||
public interface IScoreInfo : IHasOnlineID<long>, IHasNamedFiles
|
||||
public interface IScoreInfo : IHasOnlineID<long>
|
||||
{
|
||||
IUser User { get; }
|
||||
|
||||
@ -22,7 +22,7 @@ namespace osu.Game.Scoring
|
||||
|
||||
double Accuracy { get; }
|
||||
|
||||
bool HasReplay { get; }
|
||||
long LegacyOnlineID { get; }
|
||||
|
||||
DateTimeOffset Date { get; }
|
||||
|
||||
|
@ -19,6 +19,13 @@ namespace osu.Game.Scoring.Legacy
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
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")]
|
||||
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
|
||||
{
|
||||
OnlineID = score.OnlineID,
|
||||
Mods = score.APIMods,
|
||||
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),
|
||||
|
@ -101,9 +101,9 @@ namespace osu.Game.Scoring.Legacy
|
||||
byte[] compressedReplay = sr.ReadByteArray();
|
||||
|
||||
if (version >= 20140721)
|
||||
scoreInfo.OnlineID = sr.ReadInt64();
|
||||
scoreInfo.LegacyOnlineID = sr.ReadInt64();
|
||||
else if (version >= 20121008)
|
||||
scoreInfo.OnlineID = sr.ReadInt32();
|
||||
scoreInfo.LegacyOnlineID = sr.ReadInt32();
|
||||
|
||||
byte[] compressedScoreInfo = null;
|
||||
|
||||
@ -121,6 +121,7 @@ namespace osu.Game.Scoring.Legacy
|
||||
|
||||
Debug.Assert(readScore != null);
|
||||
|
||||
score.ScoreInfo.OnlineID = readScore.OnlineID;
|
||||
score.ScoreInfo.Statistics = readScore.Statistics;
|
||||
score.ScoreInfo.MaximumStatistics = readScore.MaximumStatistics;
|
||||
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(score.ScoreInfo.Date.DateTime);
|
||||
sw.WriteByteArray(createReplayData());
|
||||
sw.Write((long)0);
|
||||
sw.Write(score.ScoreInfo.LegacyOnlineID);
|
||||
writeModSpecificData(score.ScoreInfo, sw);
|
||||
sw.WriteByteArray(createScoreInfoData());
|
||||
}
|
||||
|
@ -94,15 +94,32 @@ namespace osu.Game.Scoring
|
||||
|
||||
public double Accuracy { get; set; }
|
||||
|
||||
public bool HasReplay => !string.IsNullOrEmpty(Hash);
|
||||
[Ignored]
|
||||
public bool HasOnlineReplay { get; set; }
|
||||
|
||||
public DateTimeOffset Date { 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]
|
||||
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")]
|
||||
public RealmUser RealmUser { get; set; } = null!;
|
||||
|
||||
@ -168,7 +185,6 @@ namespace osu.Game.Scoring
|
||||
IRulesetInfo IScoreInfo.Ruleset => Ruleset;
|
||||
IBeatmapInfo? IScoreInfo.Beatmap => BeatmapInfo;
|
||||
IUser IScoreInfo.User => User;
|
||||
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
|
||||
|
||||
#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 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;
|
||||
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Audio;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Screens.Edit.Timing;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -101,7 +102,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
},
|
||||
volume = new IndeterminateSliderWithTextBoxInput<int>("Volume", new BindableInt(100)
|
||||
{
|
||||
MinValue = 0,
|
||||
MinValue = DrawableHitObject.MINIMUM_SAMPLE_VOLUME,
|
||||
MaxValue = 100,
|
||||
})
|
||||
}
|
||||
|
@ -425,7 +425,7 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
if (HasUnsavedChanges)
|
||||
{
|
||||
dialogOverlay.Push(new SaveBeforeGameplayTestDialog(() =>
|
||||
dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to test it.", () =>
|
||||
{
|
||||
if (!Save()) return;
|
||||
|
||||
@ -1018,25 +1018,36 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
var exportItems = new List<MenuItem>
|
||||
{
|
||||
new EditorMenuItem(EditorStrings.ExportForEditing, MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
||||
new EditorMenuItem(EditorStrings.ExportForCompatibility, MenuItemType.Standard, exportLegacyBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
||||
new EditorMenuItem(EditorStrings.ExportForEditing, MenuItemType.Standard, () => exportBeatmap(false)) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
||||
new EditorMenuItem(EditorStrings.ExportForCompatibility, MenuItemType.Standard, () => exportBeatmap(true)) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
||||
};
|
||||
|
||||
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()
|
||||
{
|
||||
if (!Save()) return;
|
||||
|
||||
beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo);
|
||||
void runExport()
|
||||
{
|
||||
if (legacy)
|
||||
beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo);
|
||||
else
|
||||
beatmapManager.Export(Beatmap.Value.BeatmapSetInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
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;
|
||||
|
||||
@ -20,7 +20,7 @@ namespace osu.Game.Screens.Edit.GameplayTest
|
||||
new PopupDialogOkButton
|
||||
{
|
||||
Text = "Sounds good, let's go!",
|
||||
Action = saveAndPreview
|
||||
Action = saveAndAction
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
@ -10,7 +10,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
public partial class MultiplayerResultsScreen : PlaylistsResultsScreen
|
||||
{
|
||||
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 Track track;
|
||||
private Track track;
|
||||
|
||||
private readonly double skipTargetTime;
|
||||
|
||||
@ -145,7 +145,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
protected override void StartGameplayClock()
|
||||
{
|
||||
addSourceClockAdjustments();
|
||||
addAdjustmentsToTrack();
|
||||
|
||||
base.StartGameplayClock();
|
||||
|
||||
@ -186,20 +186,20 @@ namespace osu.Game.Screens.Play
|
||||
/// </summary>
|
||||
public void StopUsingBeatmapClock()
|
||||
{
|
||||
removeSourceClockAdjustments();
|
||||
removeAdjustmentsFromTrack();
|
||||
|
||||
var virtualTrack = new TrackVirtual(beatmap.Track.Length);
|
||||
virtualTrack.Seek(CurrentTime);
|
||||
track = new TrackVirtual(beatmap.Track.Length);
|
||||
track.Seek(CurrentTime);
|
||||
if (IsRunning)
|
||||
virtualTrack.Start();
|
||||
ChangeSource(virtualTrack);
|
||||
track.Start();
|
||||
ChangeSource(track);
|
||||
|
||||
addSourceClockAdjustments();
|
||||
addAdjustmentsToTrack();
|
||||
}
|
||||
|
||||
private bool speedAdjustmentsApplied;
|
||||
|
||||
private void addSourceClockAdjustments()
|
||||
private void addAdjustmentsToTrack()
|
||||
{
|
||||
if (speedAdjustmentsApplied)
|
||||
return;
|
||||
@ -213,7 +213,7 @@ namespace osu.Game.Screens.Play
|
||||
speedAdjustmentsApplied = true;
|
||||
}
|
||||
|
||||
private void removeSourceClockAdjustments()
|
||||
private void removeAdjustmentsFromTrack()
|
||||
{
|
||||
if (!speedAdjustmentsApplied)
|
||||
return;
|
||||
@ -228,7 +228,7 @@ namespace osu.Game.Screens.Play
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
removeSourceClockAdjustments();
|
||||
removeAdjustmentsFromTrack();
|
||||
}
|
||||
|
||||
ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo;
|
||||
|
@ -1110,13 +1110,14 @@ namespace osu.Game.Screens.Play
|
||||
failAnimationContainer?.Stop();
|
||||
PauseOverlay?.StopAllSamples();
|
||||
|
||||
if (LoadedBeatmapSuccessfully)
|
||||
if (LoadedBeatmapSuccessfully && !GameplayState.HasPassed)
|
||||
{
|
||||
if (!GameplayState.HasPassed && !GameplayState.HasFailed)
|
||||
Debug.Assert(resultsDisplayDelegate == null);
|
||||
|
||||
if (!GameplayState.HasFailed)
|
||||
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 (prepareScoreForDisplayTask == null && DrawableRuleset.ReplayScore == null)
|
||||
if (DrawableRuleset.ReplayScore == null)
|
||||
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.
|
||||
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);
|
||||
|
||||
imported.PerformRead(s =>
|
||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Screens.Ranking
|
||||
if (State.Value == DownloadState.LocallyAvailable)
|
||||
return ReplayAvailability.Local;
|
||||
|
||||
if (Score.Value?.HasReplay == true)
|
||||
if (Score.Value?.HasOnlineReplay == true)
|
||||
return ReplayAvailability.Online;
|
||||
|
||||
return ReplayAvailability.NotAvailable;
|
||||
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Solo;
|
||||
@ -67,7 +68,7 @@ namespace osu.Game.Screens.Ranking
|
||||
return null;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -1197,6 +1197,8 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
private bool rightMouseScrollBlocked;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
public CarouselScrollContainer()
|
||||
{
|
||||
// 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);
|
||||
match &= !criteria.Title.HasFilter || criteria.Title.Matches(BeatmapInfo.Metadata.Title) ||
|
||||
criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode);
|
||||
|
||||
match &= !criteria.DifficultyName.HasFilter || criteria.DifficultyName.Matches(BeatmapInfo.DifficultyName);
|
||||
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating);
|
||||
|
||||
if (!match) return false;
|
||||
|
@ -38,6 +38,7 @@ namespace osu.Game.Screens.Select
|
||||
public OptionalTextFilter Creator;
|
||||
public OptionalTextFilter Artist;
|
||||
public OptionalTextFilter Title;
|
||||
public OptionalTextFilter DifficultyName;
|
||||
|
||||
public OptionalRange<double> UserStarDifficulty = new OptionalRange<double>
|
||||
{
|
||||
@ -68,8 +69,23 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
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.
|
||||
foreach (Match quotedSegment in Regex.Matches(searchText, "(\"[^\"]+\"[!]?)"))
|
||||
foreach (Match quotedSegment in Regex.Matches(value, "(\"[^\"]+\"[!]?)"))
|
||||
{
|
||||
terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value });
|
||||
remainingText = remainingText.Replace(quotedSegment.Value, string.Empty);
|
||||
|
@ -76,6 +76,9 @@ namespace osu.Game.Screens.Select
|
||||
case "title":
|
||||
return TryUpdateCriteriaText(ref criteria.Title, op, value);
|
||||
|
||||
case "diff":
|
||||
return TryUpdateCriteriaText(ref criteria.DifficultyName, op, value);
|
||||
|
||||
default:
|
||||
return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false;
|
||||
}
|
||||
|
@ -171,11 +171,6 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
new ResetScrollContainer(() => Carousel.ScrollToSelected())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 250,
|
||||
},
|
||||
new VerticalMaskingContainer
|
||||
{
|
||||
Children = new Drawable[]
|
||||
@ -243,6 +238,10 @@ namespace osu.Game.Screens.Select
|
||||
Padding = new MarginPadding { Top = left_area_padding },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new LeftSideInteractionContainer(() => Carousel.ScrollToSelected())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
beatmapInfoWedge = new BeatmapInfoWedge
|
||||
{
|
||||
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)
|
||||
{
|
||||
onHoverAction?.Invoke();
|
||||
resetCarouselPosition?.Invoke();
|
||||
return base.OnHover(e);
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
FixedWidth = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
Anchor = 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)
|
||||
{
|
||||
@ -214,7 +218,7 @@ namespace osu.Game.Skinning
|
||||
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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@ -12,8 +13,10 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
public sealed partial class LegacySpriteText : OsuSpriteText
|
||||
{
|
||||
public Vector2? MaxSizePerGlyph { get; init; }
|
||||
public bool FixedWidth { get; init; }
|
||||
|
||||
private readonly LegacyFont font;
|
||||
private readonly Vector2? maxSizePerGlyph;
|
||||
|
||||
private LegacyGlyphStore glyphStore = null!;
|
||||
|
||||
@ -21,10 +24,18 @@ namespace osu.Game.Skinning
|
||||
|
||||
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.maxSizePerGlyph = maxSizePerGlyph;
|
||||
|
||||
Shadow = false;
|
||||
UseFullGlyphHeight = false;
|
||||
@ -33,10 +44,10 @@ namespace osu.Game.Skinning
|
||||
[BackgroundDependencyLoader]
|
||||
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);
|
||||
|
||||
glyphStore = new LegacyGlyphStore(skin, maxSizePerGlyph);
|
||||
glyphStore = new LegacyGlyphStore(skin, MaxSizePerGlyph);
|
||||
}
|
||||
|
||||
protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore);
|
||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
[MapTo("Skin")]
|
||||
[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 ARGON_SKIN = new Guid("CFFA69DE-B3E3-4DEE-8563-3C4F425C05D0");
|
||||
|
@ -182,7 +182,10 @@ namespace osu.Game.Skinning
|
||||
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)
|
||||
{
|
||||
|
@ -20,6 +20,12 @@ namespace osu.Game.Skinning
|
||||
/// </summary>
|
||||
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 RemoveCompletedTransforms => false;
|
||||
|
||||
@ -156,7 +162,7 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
var sample = samplePool?.GetPooledSample(s) ?? new PoolableSkinnableSample(s);
|
||||
sample.Looping = Looping;
|
||||
sample.Volume.Value = s.Volume / 100.0;
|
||||
sample.Volume.Value = Math.Max(s.Volume, MinimumSampleVolume) / 100.0;
|
||||
|
||||
samplesContainer.Add(sample);
|
||||
}
|
||||
|
@ -30,8 +30,12 @@ namespace osu.Game.Storyboards
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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>
|
||||
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>
|
||||
/// Across all layers, find the latest point in time that a storyboard element ends at.
|
||||
@ -39,9 +43,12 @@ namespace osu.Game.Storyboards
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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>
|
||||
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>
|
||||
/// Depth of the currently front-most storyboard layer, excluding the overlay layer.
|
||||
|
@ -432,6 +432,11 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
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")
|
||||
: base(name)
|
||||
{
|
||||
|
@ -37,9 +37,10 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="11.5.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="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="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.6" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
|