Merge branch 'master' into chat-report

This commit is contained in:
Dean Herbert 2023-05-03 18:50:38 +09:00
commit 4f55afb60d
241 changed files with 3008 additions and 1184 deletions

View File

@ -105,7 +105,7 @@ When it comes to contributing to the project, the two main things you can do to
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web). If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via PayPal or osu!supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project. We love to reward quality contributions. If you have made a large contribution, or are a regular contributor, you are welcome to [submit an expense via opencollective](https://opencollective.com/ppy/expenses/new). If you have any questions, feel free to [reach out to peppy](mailto:pe@ppy.sh) before doing so.
## Licence ## Licence

View File

@ -11,7 +11,7 @@
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger> <AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.327.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2023.418.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" /> <AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1])); AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0])); AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1])); AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); AddAssert("default slider velocity", () => lastObject.SliderVelocityBindable.IsDefault);
} }
[Test] [Test]
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addPlacementSteps(times, positions); addPlacementSteps(times, positions);
addPathCheckStep(times, positions); addPathCheckStep(times, positions);
AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); AddAssert("slider velocity changed", () => !lastObject.SliderVelocityBindable.IsDefault);
} }
[Test] [Test]

View File

@ -108,11 +108,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
double[] times = { 100, 300 }; double[] times = { 100, 300 };
float[] positions = { 200, 300 }; float[] positions = { 200, 300 };
addBlueprintStep(times, positions); addBlueprintStep(times, positions);
AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); AddAssert("default slider velocity", () => hitObject.SliderVelocityBindable.IsDefault);
addDragStartStep(times[1], positions[1]); addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 400); AddMouseMoveStep(times[1], 400);
AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); AddAssert("slider velocity changed", () => !hitObject.SliderVelocityBindable.IsDefault);
} }
[Test] [Test]

View File

@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
var xPositionData = obj as IHasXPosition; var xPositionData = obj as IHasXPosition;
var yPositionData = obj as IHasYPosition; var yPositionData = obj as IHasYPosition;
var comboData = obj as IHasCombo; var comboData = obj as IHasCombo;
var sliderVelocityData = obj as IHasSliderVelocity;
switch (obj) switch (obj)
{ {
@ -41,7 +42,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
NewCombo = comboData?.NewCombo ?? false, NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0, ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0, LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1
}.Yield(); }.Yield();
case IHasDuration endTime: case IHasDuration endTime:

View File

@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdateHitObjectFromPath(JuiceStream hitObject) public void UpdateHitObjectFromPath(JuiceStream hitObject)
{ {
// The SV setting may need to be changed for the current path. // The SV setting may need to be changed for the current path.
var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable; var svBindable = hitObject.SliderVelocityBindable;
double svToVelocityFactor = hitObject.Velocity / svBindable.Value; double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
double requiredVelocity = path.ComputeRequiredVelocity(); double requiredVelocity = path.ComputeRequiredVelocity();

View File

@ -48,7 +48,6 @@ namespace osu.Game.Rulesets.Catch.Edit
private void load() private void load()
{ {
// todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation. // todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation.
RightSideToolboxContainer.Alpha = 0;
DistanceSpacingMultiplier.Disabled = true; DistanceSpacingMultiplier.Disabled = true;
LayerBelowRuleset.Add(new PlayfieldBorder LayerBelowRuleset.Add(new PlayfieldBorder

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModDaycore : ModDaycore public class CatchModDaycore : ModDaycore
{ {
public override double ScoreMultiplier => 0.3;
} }
} }

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public DifficultyBindable CircleSize { get; } = new DifficultyBindable public DifficultyBindable CircleSize { get; } = new DifficultyBindable
{ {
Precision = 0.1f, Precision = 0.1f,
MinValue = 1, MinValue = 0,
MaxValue = 10, MaxValue = 10,
ExtendedMaxValue = 11, ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.CircleSize, ReadCurrentFromDifficulty = diff => diff.CircleSize,
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
{ {
Precision = 0.1f, Precision = 0.1f,
MinValue = 1, MinValue = 0,
MaxValue = 10, MaxValue = 10,
ExtendedMaxValue = 11, ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.ApproachRate, ReadCurrentFromDifficulty = diff => diff.ApproachRate,

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModDoubleTime : ModDoubleTime public class CatchModDoubleTime : ModDoubleTime
{ {
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
} }
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModHalfTime : ModHalfTime public class CatchModHalfTime : ModHalfTime
{ {
public override double ScoreMultiplier => 0.3;
} }
} }

View File

@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModNightcore : ModNightcore<CatchHitObject> public class CatchModNightcore : ModNightcore<CatchHitObject>
{ {
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
} }
} }

View File

@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Catch.Objects
public override Judgement CreateJudgement() => new CatchBananaJudgement(); public override Judgement CreateJudgement() => new CatchBananaJudgement();
private static readonly List<HitSampleInfo> samples = new List<HitSampleInfo> { new BananaHitSampleInfo() }; private static readonly IList<HitSampleInfo> default_banana_samples = new List<HitSampleInfo> { new BananaHitSampleInfo() }.AsReadOnly();
public Banana() public Banana()
{ {
Samples = samples; Samples = default_banana_samples;
} }
// override any external colour changes with banananana // override any external colour changes with banananana
@ -47,13 +47,13 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
} }
private class BananaHitSampleInfo : HitSampleInfo, IEquatable<BananaHitSampleInfo> public class BananaHitSampleInfo : HitSampleInfo, IEquatable<BananaHitSampleInfo>
{ {
private static readonly string[] lookup_names = { "Gameplay/metronomelow", "Gameplay/catch-banana" }; private static readonly string[] lookup_names = { "Gameplay/metronomelow", "Gameplay/catch-banana" };
public override IEnumerable<string> LookupNames => lookup_names; public override IEnumerable<string> LookupNames => lookup_names;
public BananaHitSampleInfo(int volume = 0) public BananaHitSampleInfo(int volume = 100)
: base(string.Empty, volume: volume) : base(string.Empty, volume: volume)
{ {
} }

View File

@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Threading; using System.Threading;
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
StartTime = time, StartTime = time,
BananaIndex = i, BananaIndex = i,
Samples = new List<HitSampleInfo> { new Banana.BananaHitSampleInfo(GetSampleInfo().Volume) }
}); });
time += spacing; time += spacing;

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -16,7 +17,7 @@ using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
{ {
public class JuiceStream : CatchHitObject, IHasPathWithRepeats public class JuiceStream : CatchHitObject, IHasPathWithRepeats, IHasSliderVelocity
{ {
/// <summary> /// <summary>
/// Positional distance that results in a duration of one second, before any speed adjustments. /// Positional distance that results in a duration of one second, before any speed adjustments.
@ -27,6 +28,19 @@ namespace osu.Game.Rulesets.Catch.Objects
public int RepeatCount { get; set; } public int RepeatCount { get; set; }
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
};
public double SliderVelocity
{
get => SliderVelocityBindable.Value;
set => SliderVelocityBindable.Value = value;
}
[JsonIgnore] [JsonIgnore]
private double velocityFactor; private double velocityFactor;
@ -34,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects
private double tickDistanceFactor; private double tickDistanceFactor;
[JsonIgnore] [JsonIgnore]
public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity; public double Velocity => velocityFactor * SliderVelocity;
[JsonIgnore] [JsonIgnore]
public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity; public double TickDistance => tickDistanceFactor * SliderVelocity;
/// <summary> /// <summary>
/// The length of one span of this <see cref="JuiceStream"/>. /// The length of one span of this <see cref="JuiceStream"/>.

View File

@ -136,6 +136,7 @@ namespace osu.Game.Rulesets.Catch.UI
Origin = Anchor.TopCentre; Origin = Anchor.TopCentre;
Size = new Vector2(BASE_SIZE); Size = new Vector2(BASE_SIZE);
if (difficulty != null) if (difficulty != null)
Scale = calculateScale(difficulty); Scale = calculateScale(difficulty);
@ -333,8 +334,11 @@ namespace osu.Game.Rulesets.Catch.UI
base.Update(); base.Update();
var scaleFromDirection = new Vector2((int)VisualDirection, 1); var scaleFromDirection = new Vector2((int)VisualDirection, 1);
body.Scale = scaleFromDirection; body.Scale = scaleFromDirection;
caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One; // Inverse of catcher scale is applied here, as catcher gets scaled by circle size and so do the incoming fruit.
caughtObjectContainer.Scale = (1 / Scale.X) * (flipCatcherPlate ? scaleFromDirection : Vector2.One);
hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
// Correct overshooting. // Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) || if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
@ -414,10 +418,13 @@ namespace osu.Game.Rulesets.Catch.UI
private void clearPlate(DroppedObjectAnimation animation) private void clearPlate(DroppedObjectAnimation animation)
{ {
var droppedObjects = caughtObjectContainer.Children.Select(getDroppedObject).ToArray(); var caughtObjects = caughtObjectContainer.Children.ToArray();
caughtObjectContainer.Clear(false); caughtObjectContainer.Clear(false);
// Use the already returned PoolableDrawables for new objects
var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray();
droppedObjectTarget.AddRange(droppedObjects); droppedObjectTarget.AddRange(droppedObjects);
foreach (var droppedObject in droppedObjects) foreach (var droppedObject in droppedObjects)
@ -426,10 +433,10 @@ namespace osu.Game.Rulesets.Catch.UI
private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation) private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation)
{ {
var droppedObject = getDroppedObject(caughtObject);
caughtObjectContainer.Remove(caughtObject, false); caughtObjectContainer.Remove(caughtObject, false);
var droppedObject = getDroppedObject(caughtObject);
droppedObjectTarget.Add(droppedObject); droppedObjectTarget.Add(droppedObject);
applyDropAnimation(droppedObject, animation); applyDropAnimation(droppedObject, animation);
@ -452,6 +459,8 @@ namespace osu.Game.Rulesets.Catch.UI
break; break;
} }
// Define lifetime start for dropped objects to be disposed correctly when rewinding replay
d.LifetimeStart = Clock.CurrentTime;
d.Expire(); d.Expire();
} }

View File

@ -14,7 +14,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Utils; using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
@ -49,15 +48,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
Debug.Assert(distanceData != null); Debug.Assert(distanceData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint;
double beatLength; double beatLength;
#pragma warning disable 618 if (hitObject.LegacyBpmMultiplier.HasValue)
if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint) beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value;
#pragma warning restore 618 else if (hitObject is IHasSliderVelocity hasSliderVelocity)
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier; beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
else else
beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity; beatLength = timingPoint.BeatLength;
SpanCount = repeatsData?.SpanCount() ?? 1; SpanCount = repeatsData?.SpanCount() ?? 1;
StartTime = (int)Math.Round(hitObject.StartTime); StartTime = (int)Math.Round(hitObject.StartTime);

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModDaycore : ModDaycore public class ManiaModDaycore : ModDaycore
{ {
public override double ScoreMultiplier => 0.5;
} }
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModDoubleTime : ModDoubleTime public class ManiaModDoubleTime : ModDoubleTime
{ {
public override double ScoreMultiplier => 1;
} }
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModHalfTime : ModHalfTime public class ManiaModHalfTime : ModHalfTime
{ {
public override double ScoreMultiplier => 0.5;
} }
} }

View File

@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModNightcore : ModNightcore<ManiaHitObject> public class ManiaModNightcore : ModNightcore<ManiaHitObject>
{ {
public override double ScoreMultiplier => 1;
} }
} }

View File

@ -350,13 +350,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being. // Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
if (HitObject.SampleControlPoint == null) slidingSample.Samples = HitObject.CreateSlidingSamples().Cast<ISampleInfo>().ToArray();
{
throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
} }
public override void StopAllSamples() public override void StopAllSamples()

View File

@ -10,7 +10,7 @@
["Gameplay/soft-hitnormal"], ["Gameplay/soft-hitnormal"],
["Gameplay/drum-hitnormal"] ["Gameplay/drum-hitnormal"]
], ],
"Samples": ["Gameplay/-hitnormal"] "Samples": ["Gameplay/normal-hitnormal"]
}, { }, {
"StartTime": 1875.0, "StartTime": 1875.0,
"EndTime": 2750.0, "EndTime": 2750.0,
@ -19,7 +19,7 @@
["Gameplay/soft-hitnormal"], ["Gameplay/soft-hitnormal"],
["Gameplay/drum-hitnormal"] ["Gameplay/drum-hitnormal"]
], ],
"Samples": ["Gameplay/-hitnormal"] "Samples": ["Gameplay/normal-hitnormal"]
}] }]
}, { }, {
"StartTime": 3750.0, "StartTime": 3750.0,

View File

@ -138,8 +138,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples) return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples) && mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
&& mergedSlider.Samples.SequenceEqual(slider1.Samples) && mergedSlider.Samples.SequenceEqual(slider1.Samples);
&& mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint);
}); });
AddAssert("slider end is at same completion for last slider", () => AddAssert("slider end is at same completion for last slider", () =>

View File

@ -181,10 +181,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
if (slider is null) return; if (slider is null) return;
slider.SampleControlPoint.SampleBank = "soft"; sample = new HitSampleInfo("hitwhistle", "soft", volume: 70);
slider.SampleControlPoint.SampleVolume = 70; slider.Samples.Add(sample.With());
sample = new HitSampleInfo("hitwhistle");
slider.Samples.Add(sample);
}); });
AddStep("select added slider", () => AddStep("select added slider", () =>
@ -207,9 +205,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("sliders have hitsounds", hasHitsounds); AddAssert("sliders have hitsounds", hasHitsounds);
bool hasHitsounds() => sample is not null && bool hasHitsounds() => sample is not null &&
EditorBeatmap.HitObjects.All(o => o.SampleControlPoint.SampleBank == "soft" && EditorBeatmap.HitObjects.All(o => o.Samples.Contains(sample));
o.SampleControlPoint.SampleVolume == 70 &&
o.Samples.Contains(sample));
} }
private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints) private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints)

View File

@ -199,8 +199,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Precision.AlmostEquals(circle.StartTime, time, 1) Precision.AlmostEquals(circle.StartTime, time, 1)
&& Precision.AlmostEquals(circle.Position, position, 0.01f) && Precision.AlmostEquals(circle.Position, position, 0.01f)
&& circle.NewCombo == startsNewCombo && circle.NewCombo == startsNewCombo
&& circle.Samples.SequenceEqual(slider.HeadCircle.Samples) && circle.Samples.SequenceEqual(slider.HeadCircle.Samples);
&& circle.SampleControlPoint.IsRedundant(slider.SampleControlPoint);
} }
private bool sliderRestored(Slider slider) private bool sliderRestored(Slider slider)

View File

@ -0,0 +1,19 @@
// 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.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModBubbles : OsuModTestScene
{
[Test]
public void TestOsuModBubbles() => CreateModTest(new ModTestData
{
Mod = new OsuModBubbles(),
Autoplay = true,
PassCondition = () => true
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -18,6 +19,7 @@ using osu.Framework.Testing.Input;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -40,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable background; private Drawable background;
private readonly Bindable<bool> ripples = new Bindable<bool>();
public TestSceneGameplayCursor() public TestSceneGameplayCursor()
{ {
var ruleset = new OsuRuleset(); var ruleset = new OsuRuleset();
@ -57,6 +61,8 @@ namespace osu.Game.Rulesets.Osu.Tests
}); });
}); });
AddToggleStep("ripples", v => ripples.Value = v);
AddSliderStep("circle size", 0f, 10f, 0f, val => AddSliderStep("circle size", 0f, 10f, 0f, val =>
{ {
config.SetValue(OsuSetting.AutoCursorSize, true); config.SetValue(OsuSetting.AutoCursorSize, true);
@ -67,6 +73,13 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("test cursor container", () => loadContent(false)); AddStep("test cursor container", () => loadContent(false));
} }
[BackgroundDependencyLoader]
private void load()
{
var rulesetConfig = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
rulesetConfig.BindWith(OsuRulesetSetting.ShowCursorRipples, ripples);
}
[TestCase(1, 1)] [TestCase(1, 1)]
[TestCase(5, 1)] [TestCase(5, 1)]
[TestCase(10, 1)] [TestCase(10, 1)]

View File

@ -439,7 +439,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public TestSlider() public TestSlider()
{ {
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }; SliderVelocity = 0.1f;
DefaultsApplied += _ => DefaultsApplied += _ =>
{ {

View File

@ -21,7 +21,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
private TestActionKeyCounter leftKeyCounter = null!; private DefaultKeyCounter leftKeyCounter = null!;
private TestActionKeyCounter rightKeyCounter = null!; private DefaultKeyCounter rightKeyCounter = null!;
private OsuInputManager osuInputManager = null!; private OsuInputManager osuInputManager = null!;
@ -59,14 +59,14 @@ namespace osu.Game.Rulesets.Osu.Tests
Origin = Anchor.Centre, Origin = Anchor.Centre,
Children = new Drawable[] Children = new Drawable[]
{ {
leftKeyCounter = new TestActionKeyCounter(OsuAction.LeftButton) leftKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.LeftButton))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Depth = float.MinValue, Depth = float.MinValue,
X = -100, X = -100,
}, },
rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton) rightKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.RightButton))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
@ -598,8 +598,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private void assertKeyCounter(int left, int right) private void assertKeyCounter(int left, int right)
{ {
AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses, () => Is.EqualTo(left)); AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses.Value, () => Is.EqualTo(left));
AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses, () => Is.EqualTo(right)); AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses.Value, () => Is.EqualTo(right));
} }
private void releaseAllTouches() private void releaseAllTouches()
@ -615,11 +615,11 @@ namespace osu.Game.Rulesets.Osu.Tests
private void checkNotPressed(OsuAction action) => AddAssert($"Not pressing {action}", () => !osuInputManager.PressedActions.Contains(action)); private void checkNotPressed(OsuAction action) => AddAssert($"Not pressing {action}", () => !osuInputManager.PressedActions.Contains(action));
private void checkPressed(OsuAction action) => AddAssert($"Is pressing {action}", () => osuInputManager.PressedActions.Contains(action)); private void checkPressed(OsuAction action) => AddAssert($"Is pressing {action}", () => osuInputManager.PressedActions.Contains(action));
public partial class TestActionKeyCounter : KeyCounter, IKeyBindingHandler<OsuAction> public partial class TestActionKeyCounterTrigger : InputTrigger, IKeyBindingHandler<OsuAction>
{ {
public OsuAction Action { get; } public OsuAction Action { get; }
public TestActionKeyCounter(OsuAction action) public TestActionKeyCounterTrigger(OsuAction action)
: base(action.ToString()) : base(action.ToString())
{ {
Action = action; Action = action;
@ -629,8 +629,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
if (e.Action == Action) if (e.Action == Action)
{ {
IsLit = true; Activate();
Increment();
} }
return false; return false;
@ -638,7 +637,8 @@ namespace osu.Game.Rulesets.Osu.Tests
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e) public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{ {
if (e.Action == Action) IsLit = false; if (e.Action == Action)
Deactivate();
} }
} }

View File

@ -7,7 +7,6 @@ using NUnit.Framework;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
StartTime = time_slider_start, StartTime = time_slider_start,
Position = new Vector2(0, 0), Position = new Vector2(0, 0),
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = velocity }, SliderVelocity = velocity,
Path = new SliderPath(PathType.Linear, new[] Path = new SliderPath(PathType.Linear, new[]
{ {
Vector2.Zero, Vector2.Zero,

View File

@ -8,7 +8,6 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -350,7 +349,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
StartTime = time_slider_start, StartTime = time_slider_start,
Position = new Vector2(0, 0), Position = new Vector2(0, 0),
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }, SliderVelocity = 0.1f,
Path = new SliderPath(PathType.PerfectCurve, new[] Path = new SliderPath(PathType.PerfectCurve, new[]
{ {
Vector2.Zero, Vector2.Zero,

View File

@ -399,7 +399,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public TestSlider() public TestSlider()
{ {
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }; SliderVelocity = 0.1f;
DefaultsApplied += _ => DefaultsApplied += _ =>
{ {

View File

@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{ {
var positionData = original as IHasPosition; var positionData = original as IHasPosition;
var comboData = original as IHasCombo; var comboData = original as IHasCombo;
var sliderVelocityData = original as IHasSliderVelocity;
var generateTicksData = original as IHasGenerateTicks;
switch (original) switch (original)
{ {
@ -47,7 +49,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset, LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance. // prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration. // this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1 TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1,
}.Yield(); }.Yield();
case IHasDuration endTimeData: case IHasDuration endTimeData:

View File

@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
SetDefault(OsuRulesetSetting.SnakingInSliders, true); SetDefault(OsuRulesetSetting.SnakingInSliders, true);
SetDefault(OsuRulesetSetting.SnakingOutSliders, true); SetDefault(OsuRulesetSetting.SnakingOutSliders, true);
SetDefault(OsuRulesetSetting.ShowCursorTrail, true); SetDefault(OsuRulesetSetting.ShowCursorTrail, true);
SetDefault(OsuRulesetSetting.ShowCursorRipples, false);
SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None); SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
} }
} }
@ -31,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
SnakingInSliders, SnakingInSliders,
SnakingOutSliders, SnakingOutSliders,
ShowCursorTrail, ShowCursorTrail,
ShowCursorRipples,
PlayfieldBorderStyle, PlayfieldBorderStyle,
} }
} }

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
protected override bool AlwaysShowWhenSelected => true; protected override bool AlwaysShowWhenSelected => true;
protected override bool ShouldBeAlive => base.ShouldBeAlive protected override bool ShouldBeAlive => base.ShouldBeAlive
|| (ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION); || (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
protected OsuSelectionBlueprint(T hitObject) protected OsuSelectionBlueprint(T hitObject)
: base(hitObject) : base(hitObject)

View File

@ -10,7 +10,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -83,11 +82,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.Initial: case SliderPlacementState.Initial:
BeginPlacement(); BeginPlacement();
var nearestDifficultyPoint = editorBeatmap.HitObjects double? nearestSliderVelocity = (editorBeatmap.HitObjects
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime)? .LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocity;
.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint(); HitObject.SliderVelocity = nearestSliderVelocity ?? 1;
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation. // Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.

View File

@ -14,7 +14,6 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -311,17 +310,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var splitControlPoints = controlPoints.Take(index + 1).ToList(); var splitControlPoints = controlPoints.Take(index + 1).ToList();
controlPoints.RemoveRange(0, index); controlPoints.RemoveRange(0, index);
// Turn the control points which were split off into a new slider.
var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone();
var newSlider = new Slider var newSlider = new Slider
{ {
StartTime = HitObject.StartTime, StartTime = HitObject.StartTime,
Position = HitObject.Position + splitControlPoints[0].Position, Position = HitObject.Position + splitControlPoints[0].Position,
NewCombo = HitObject.NewCombo, NewCombo = HitObject.NewCombo,
SampleControlPoint = samplePoint,
DifficultyControlPoint = difficultyPoint,
LegacyLastTickOffset = HitObject.LegacyLastTickOffset, LegacyLastTickOffset = HitObject.LegacyLastTickOffset,
Samples = HitObject.Samples.Select(s => s.With()).ToList(), Samples = HitObject.Samples.Select(s => s.With()).ToList(),
RepeatCount = HitObject.RepeatCount, RepeatCount = HitObject.RepeatCount,
@ -378,15 +371,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition); Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);
var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
samplePoint.Time = time;
editorBeatmap.Add(new HitCircle editorBeatmap.Add(new HitCircle
{ {
StartTime = time, StartTime = time,
Position = position, Position = position,
NewCombo = i == 0 && HitObject.NewCombo, NewCombo = i == 0 && HitObject.NewCombo,
SampleControlPoint = samplePoint,
Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList() Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList()
}); });

View File

@ -13,8 +13,8 @@ using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
@ -62,7 +62,12 @@ namespace osu.Game.Rulesets.Osu.Edit
private void load() private void load()
{ {
// Give a bit of breathing room around the playfield content. // Give a bit of breathing room around the playfield content.
PlayfieldContentContainer.Padding = new MarginPadding(10); PlayfieldContentContainer.Padding = new MarginPadding
{
Vertical = 10,
Left = TOOLBOX_CONTRACTED_SIZE_LEFT + 10,
Right = TOOLBOX_CONTRACTED_SIZE_RIGHT + 10,
};
LayerBelowRuleset.AddRange(new Drawable[] LayerBelowRuleset.AddRange(new Drawable[]
{ {

View File

@ -362,7 +362,6 @@ namespace osu.Game.Rulesets.Osu.Edit
StartTime = firstHitObject.StartTime, StartTime = firstHitObject.StartTime,
Position = firstHitObject.Position, Position = firstHitObject.Position,
NewCombo = firstHitObject.NewCombo, NewCombo = firstHitObject.NewCombo,
SampleControlPoint = firstHitObject.SampleControlPoint,
Samples = firstHitObject.Samples, Samples = firstHitObject.Samples,
}; };

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -10,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModBarrelRoll : ModBarrelRoll<OsuHitObject>, IApplicableToDrawableHitObject public class OsuModBarrelRoll : ModBarrelRoll<OsuHitObject>, IApplicableToDrawableHitObject
{ {
public override Type[] IncompatibleMods => new[] { typeof(OsuModBubbles) };
public void ApplyToDrawableHitObject(DrawableHitObject d) public void ApplyToDrawableHitObject(DrawableHitObject d)
{ {
d.OnUpdate += _ => d.OnUpdate += _ =>

View File

@ -0,0 +1,214 @@
// 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.Diagnostics;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
public partial class OsuModBubbles : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToDrawableHitObject, IApplicableToScoreProcessor
{
public override string Name => "Bubbles";
public override string Acronym => "BU";
public override LocalisableString Description => "Don't let their popping distract you!";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.Fun;
// Compatibility with these seems potentially feasible in the future, blocked for now because they don't work as one would expect
public override Type[] IncompatibleMods => new[] { typeof(OsuModBarrelRoll), typeof(OsuModMagnetised), typeof(OsuModRepel) };
private PlayfieldAdjustmentContainer bubbleContainer = null!;
private readonly Bindable<int> currentCombo = new BindableInt();
private float maxSize;
private float bubbleSize;
private double bubbleFade;
private readonly DrawablePool<BubbleDrawable> bubblePool = new DrawablePool<BubbleDrawable>(100);
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
{
currentCombo.BindTo(scoreProcessor.Combo);
currentCombo.BindValueChanged(combo =>
maxSize = Math.Min(1.75f, (float)(1.25 + 0.005 * combo.NewValue)), true);
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// Multiplying by 2 results in an initial size that is too large, hence 1.90 has been chosen
// Also avoids the HitObject bleeding around the edges of the bubble drawable at minimum size
bubbleSize = (float)(drawableRuleset.Beatmap.HitObjects.OfType<HitCircle>().First().Radius * 1.90f);
bubbleFade = drawableRuleset.Beatmap.HitObjects.OfType<HitCircle>().First().TimePreempt * 2;
// We want to hide the judgements since they are obscured by the BubbleDrawable (due to layering)
drawableRuleset.Playfield.DisplayJudgements.Value = false;
bubbleContainer = drawableRuleset.CreatePlayfieldAdjustmentContainer();
drawableRuleset.Overlays.Add(bubbleContainer);
}
public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)
{
drawableObject.OnNewResult += (drawable, _) =>
{
if (drawable is not DrawableOsuHitObject drawableOsuHitObject) return;
switch (drawableOsuHitObject.HitObject)
{
case Slider:
case SpinnerTick:
break;
default:
addBubble();
break;
}
void addBubble()
{
BubbleDrawable bubble = bubblePool.Get();
bubble.DrawableOsuHitObject = drawableOsuHitObject;
bubble.InitialSize = new Vector2(bubbleSize);
bubble.FadeTime = bubbleFade;
bubble.MaxSize = maxSize;
bubbleContainer.Add(bubble);
}
};
drawableObject.OnRevertResult += (drawable, _) =>
{
if (drawable.HitObject is SpinnerTick or Slider) return;
BubbleDrawable? lastBubble = bubbleContainer.OfType<BubbleDrawable>().LastOrDefault();
lastBubble?.ClearTransforms();
lastBubble?.Expire(true);
};
}
#region Pooled Bubble drawable
private partial class BubbleDrawable : PoolableDrawable
{
public DrawableOsuHitObject? DrawableOsuHitObject { get; set; }
public Vector2 InitialSize { get; set; }
public float MaxSize { get; set; }
public double FadeTime { get; set; }
private readonly Box colourBox;
private readonly CircularContainer content;
public BubbleDrawable()
{
Origin = Anchor.Centre;
InternalChild = content = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
MaskingSmoothness = 2,
BorderThickness = 0,
BorderColour = Colour4.White,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 3,
Colour = Colour4.Black.Opacity(0.05f),
},
Child = colourBox = new Box { RelativeSizeAxes = Axes.Both, }
};
}
protected override void PrepareForUse()
{
Debug.Assert(DrawableOsuHitObject.IsNotNull());
Colour = DrawableOsuHitObject.IsHit ? Colour4.White : Colour4.Black;
Scale = new Vector2(1);
Position = getPosition(DrawableOsuHitObject);
Size = InitialSize;
//We want to fade to a darker colour to avoid colours such as white hiding the "ripple" effect.
ColourInfo colourDarker = DrawableOsuHitObject.AccentColour.Value.Darken(0.1f);
// The absolute length of the bubble's animation, can be used in fractions for animations of partial length
double duration = 1700 + Math.Pow(FadeTime, 1.07f);
// Main bubble scaling based on combo
this.FadeTo(1)
.ScaleTo(MaxSize, duration * 0.8f)
.Then()
// Pop at the end of the bubbles life time
.ScaleTo(MaxSize * 1.5f, duration * 0.2f, Easing.OutQuint)
.FadeOut(duration * 0.2f, Easing.OutCirc).Expire();
if (!DrawableOsuHitObject.IsHit) return;
content.BorderThickness = InitialSize.X / 3.5f;
content.BorderColour = Colour4.White;
colourBox.FadeColour(colourDarker);
content.TransformTo(nameof(BorderColour), colourDarker, duration * 0.3f, Easing.OutQuint);
// Ripple effect utilises the border to reduce drawable count
content.TransformTo(nameof(BorderThickness), 2f, duration * 0.3f, Easing.OutQuint)
.Then()
// Avoids transparency overlap issues during the bubble "pop"
.TransformTo(nameof(BorderThickness), 0f);
}
private Vector2 getPosition(DrawableOsuHitObject drawableObject)
{
switch (drawableObject)
{
// SliderHeads are derived from HitCircles,
// so we must handle them before to avoid them using the wrong positioning logic
case DrawableSliderHead:
return drawableObject.HitObject.Position;
// Using hitobject position will cause issues with HitCircle placement due to stack leniency.
case DrawableHitCircle:
return drawableObject.Position;
default:
return drawableObject.HitObject.Position;
}
}
}
#endregion
}
}

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModDaycore : ModDaycore public class OsuModDaycore : ModDaycore
{ {
public override double ScoreMultiplier => 0.3;
} }
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModDoubleTime : ModDoubleTime public class OsuModDoubleTime : ModDoubleTime
{ {
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
} }
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModHalfTime : ModHalfTime public class OsuModHalfTime : ModHalfTime
{ {
public override double ScoreMultiplier => 0.3;
} }
} }

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override LocalisableString Description => "No need to chase the circles your cursor is a magnet!"; public override LocalisableString Description => "No need to chase the circles your cursor is a magnet!";
public override double ScoreMultiplier => 0.5; public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles) };
[SettingSource("Attraction strength", "How strong the pull is.", 0)] [SettingSource("Attraction strength", "How strong the pull is.", 0)]
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f) public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)

View File

@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModNightcore : ModNightcore<OsuHitObject> public class OsuModNightcore : ModNightcore<OsuHitObject>
{ {
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
} }
} }

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Hit objects run away!"; public override LocalisableString Description => "Hit objects run away!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles) };
[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)] [SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f) public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)

View File

@ -133,14 +133,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being. // Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
if (HitObject.SampleControlPoint == null) Samples.Samples = HitObject.TailSamples.Cast<ISampleInfo>().ToArray();
{ slidingSample.Samples = HitObject.CreateSlidingSamples().Cast<ISampleInfo>().ToArray();
throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
} }
public override void StopAllSamples() public override void StopAllSamples()

View File

@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
base.LoadSamples(); base.LoadSamples();
spinningSample.Samples = HitObject.CreateSpinningSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray(); spinningSample.Samples = HitObject.CreateSpinningSamples().Cast<ISampleInfo>().ToArray();
spinningSample.Frequency.Value = spinning_sample_initial_frequency; spinningSample.Frequency.Value = spinning_sample_initial_frequency;
} }

View File

@ -10,18 +10,18 @@ using osu.Game.Rulesets.Objects;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects namespace osu.Game.Rulesets.Osu.Objects
{ {
public class Slider : OsuHitObject, IHasPathWithRepeats public class Slider : OsuHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks
{ {
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
@ -134,6 +134,21 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary> /// </summary>
public bool OnlyJudgeNestedObjects = true; public bool OnlyJudgeNestedObjects = true;
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
};
public double SliderVelocity
{
get => SliderVelocityBindable.Value;
set => SliderVelocityBindable.Value = value;
}
public bool GenerateTicks { get; set; } = true;
[JsonIgnore] [JsonIgnore]
public SliderHeadCircle HeadCircle { get; protected set; } public SliderHeadCircle HeadCircle { get; protected set; }
@ -151,15 +166,11 @@ namespace osu.Game.Rulesets.Osu.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
#pragma warning disable 618
var legacyDifficultyPoint = DifficultyControlPoint as LegacyBeatmapDecoder.LegacyDifficultyControlPoint;
#pragma warning restore 618
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * SliderVelocity;
bool generateTicks = legacyDifficultyPoint?.GenerateTicks ?? true;
Velocity = scoringDistance / timingPoint.BeatLength; Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = generateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity; TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
} }
protected override void CreateNestedHitObjects(CancellationToken cancellationToken) protected override void CreateNestedHitObjects(CancellationToken cancellationToken)

View File

@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Objects
AddNested(i < SpinsRequired AddNested(i < SpinsRequired
? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration } ? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
: new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration }); : new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration, Samples = new[] { GetSampleInfo("spinnerbonus") } });
} }
} }
@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects
return new[] return new[]
{ {
SampleControlPoint.ApplyTo(referenceSample).With("spinnerspin") referenceSample.With("spinnerspin")
}; };
} }
} }

View File

@ -3,7 +3,6 @@
#nullable disable #nullable disable
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -11,11 +10,6 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
public class SpinnerBonusTick : SpinnerTick public class SpinnerBonusTick : SpinnerTick
{ {
public SpinnerBonusTick()
{
Samples.Add(new HitSampleInfo("spinnerbonus"));
}
public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement(); public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement

View File

@ -203,7 +203,8 @@ namespace osu.Game.Rulesets.Osu
new OsuModNoScope(), new OsuModNoScope(),
new MultiMod(new OsuModMagnetised(), new OsuModRepel()), new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
new ModAdaptiveSpeed(), new ModAdaptiveSpeed(),
new OsuModFreezeFrame() new OsuModFreezeFrame(),
new OsuModBubbles()
}; };
case ModType.System: case ModType.System:

View File

@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Osu
Cursor, Cursor,
CursorTrail, CursorTrail,
CursorParticles, CursorParticles,
CursorRipple,
SliderScorePoint, SliderScorePoint,
ReverseArrow, ReverseArrow,
HitCircleText, HitCircleText,

View File

@ -98,7 +98,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{ {
this.FadeOut(duration, Easing.OutQuint); // intentionally pile on an extra FadeOut to make it happen much faster
this.FadeOut(duration / 4, Easing.OutQuint);
icon.ScaleTo(defaultIconScale * icon_scale, duration, Easing.OutQuint); icon.ScaleTo(defaultIconScale * icon_scale, duration, Easing.OutQuint);
} }
} }

View File

@ -3,11 +3,13 @@
using System.Diagnostics; using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
@ -18,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private Drawable proxy = null!; private Drawable proxy = null!;
private Bindable<Color4> accentColour = null!;
private bool textureIsDefaultSkin;
private Drawable arrow = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skinSource) private void load(ISkinSource skinSource)
{ {
@ -26,7 +34,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
string lookupName = new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow).LookupName; string lookupName = new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow).LookupName;
var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null); var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null);
InternalChild = skin?.GetAnimation(lookupName, true, true) ?? Empty();
InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true) ?? Empty());
textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -39,6 +49,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
drawableHitObject.HitObjectApplied += onHitObjectApplied; drawableHitObject.HitObjectApplied += onHitObjectApplied;
onHitObjectApplied(drawableHitObject); onHitObjectApplied(drawableHitObject);
accentColour = drawableHitObject.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(c =>
{
arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White;
}, true);
} }
} }

View File

@ -100,6 +100,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null; return null;
case OsuSkinComponents.CursorRipple:
if (GetTexture("cursor-ripple") != null)
{
var ripple = this.GetAnimation("cursor-ripple", false, false);
// In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible.
// If anyone complains about these not being applied, this can be uncommented.
//
// But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size,
// so we might be okay.
//
// if (ripple != null)
// {
// ripple.Scale = new Vector2(0.5f);
// ripple.Alpha = 0.2f;
// }
return ripple;
}
return null;
case OsuSkinComponents.CursorParticles: case OsuSkinComponents.CursorParticles:
if (GetTexture("star2") != null) if (GetTexture("star2") != null)
return new LegacyCursorParticles(); return new LegacyCursorParticles();

View File

@ -0,0 +1,105 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI.Cursor
{
public partial class CursorRippleVisualiser : CompositeDrawable, IKeyBindingHandler<OsuAction>
{
private readonly Bindable<bool> showRipples = new Bindable<bool>(true);
private readonly DrawablePool<CursorRipple> ripplePool = new DrawablePool<CursorRipple>(20);
public CursorRippleVisualiser()
{
RelativeSizeAxes = Axes.Both;
}
public Vector2 CursorScale { get; set; } = Vector2.One;
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager? rulesetConfig)
{
rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorRipples, showRipples);
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{
if (showRipples.Value)
{
AddInternal(ripplePool.Get(r =>
{
r.Position = e.MousePosition;
r.Scale = CursorScale;
}));
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
private partial class CursorRipple : PoolableDrawable
{
private Drawable ripple = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
InternalChild = ripple = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorRipple), _ => new DefaultCursorRipple())
{
Blending = BlendingParameters.Additive,
};
}
protected override void PrepareForUse()
{
base.PrepareForUse();
ClearTransforms(true);
ripple.ScaleTo(0.1f)
.ScaleTo(1, 700, Easing.Out);
this
.FadeOutFromOne(700)
.Expire(true);
}
}
public partial class DefaultCursorRipple : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new RingPiece(3)
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2),
Alpha = 0.1f,
}
};
}
}
}
}

View File

@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private Bindable<float> userCursorScale; private Bindable<float> userCursorScale;
private Bindable<bool> autoCursorScale; private Bindable<bool> autoCursorScale;
private readonly CursorRippleVisualiser rippleVisualiser;
public OsuCursorContainer() public OsuCursorContainer()
{ {
InternalChild = fadeContainer = new Container InternalChild = fadeContainer = new Container
@ -48,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Children = new[] Children = new[]
{ {
cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling), cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
rippleVisualiser = new CursorRippleVisualiser(),
new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorParticles), confineMode: ConfineMode.NoScaling), new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorParticles), confineMode: ConfineMode.NoScaling),
} }
}; };
@ -82,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
var newScale = new Vector2(e.NewValue); var newScale = new Vector2(e.NewValue);
ActiveCursor.Scale = newScale; ActiveCursor.Scale = newScale;
rippleVisualiser.CursorScale = newScale;
cursorTrail.Scale = newScale; cursorTrail.Scale = newScale;
}, true); }, true);

View File

@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Osu.UI
LabelText = RulesetSettingsStrings.CursorTrail, LabelText = RulesetSettingsStrings.CursorTrail,
Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail) Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail)
}, },
new SettingsCheckbox
{
LabelText = RulesetSettingsStrings.CursorRipples,
Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorRipples)
},
new SettingsEnumDropdown<PlayfieldBorderStyle> new SettingsEnumDropdown<PlayfieldBorderStyle>
{ {
LabelText = RulesetSettingsStrings.PlayfieldBorderStyle, LabelText = RulesetSettingsStrings.PlayfieldBorderStyle,

View File

@ -64,7 +64,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
foreach (HitObject hitObject in original.HitObjects) foreach (HitObject hitObject in original.HitObjects)
{ {
double nextScrollSpeed = hitObject.DifficultyControlPoint.SliderVelocity; if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue;
double nextScrollSpeed = hasSliderVelocity.SliderVelocity;
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime); EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision)) if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
@ -131,7 +133,8 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
StartTime = obj.StartTime, StartTime = obj.StartTime,
Samples = obj.Samples, Samples = obj.Samples,
Duration = taikoDuration, Duration = taikoDuration,
TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4 TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4,
SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1
}; };
} }
@ -177,15 +180,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime); TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
DifficultyControlPoint difficultyPoint = obj.DifficultyControlPoint;
double beatLength; double beatLength;
#pragma warning disable 618 if (obj.LegacyBpmMultiplier.HasValue)
if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint) beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value;
#pragma warning restore 618 else if (obj is IHasSliderVelocity hasSliderVelocity)
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier; beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
else else
beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity; beatLength = timingPoint.BeatLength;
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate; double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
@ -35,20 +33,11 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
switch (e.Button) if (e.Button != MouseButton.Left)
{ return false;
case MouseButton.Left:
HitObject.Type = HitType.Centre;
EndPlacement(true);
return true;
case MouseButton.Right: EndPlacement(true);
HitObject.Type = HitType.Rim; return true;
EndPlacement(true);
return true;
}
return false;
} }
public override void UpdateTimeAndPosition(SnapResult result) public override void UpdateTimeAndPosition(SnapResult result)

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{ {
var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false; drawableTaikoRuleset.LockPlayfieldAspectRange.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield; var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true; playfield.ClassicHitTargetPosition.Value = true;

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModDaycore : ModDaycore public class TaikoModDaycore : ModDaycore
{ {
public override double ScoreMultiplier => 0.3;
} }
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModDoubleTime : ModDoubleTime public class TaikoModDoubleTime : ModDoubleTime
{ {
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
} }
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModHalfTime : ModHalfTime public class TaikoModHalfTime : ModHalfTime
{ {
public override double ScoreMultiplier => 0.3;
} }
} }

View File

@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModNightcore : ModNightcore<TaikoHitObject> public class TaikoModNightcore : ModNightcore<TaikoHitObject>
{ {
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
} }
} }

View File

@ -3,8 +3,11 @@
#nullable disable #nullable disable
using System.Linq;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System.Threading; using System.Threading;
using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
@ -15,7 +18,7 @@ using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects namespace osu.Game.Rulesets.Taiko.Objects
{ {
public class DrumRoll : TaikoStrongableHitObject, IHasPath public class DrumRoll : TaikoStrongableHitObject, IHasPath, IHasSliderVelocity
{ {
/// <summary> /// <summary>
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length. /// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
@ -35,6 +38,19 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// </summary> /// </summary>
public double Velocity { get; private set; } public double Velocity { get; private set; }
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
};
public double SliderVelocity
{
get => SliderVelocityBindable.Value;
set => SliderVelocityBindable.Value = value;
}
/// <summary> /// <summary>
/// Numer of ticks per beat length. /// Numer of ticks per beat length.
/// </summary> /// </summary>
@ -52,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
double scoringDistance = base_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength; Velocity = scoringDistance / timingPoint.BeatLength;
tickSpacing = timingPoint.BeatLength / TickRate; tickSpacing = timingPoint.BeatLength / TickRate;
@ -81,7 +97,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
FirstTick = first, FirstTick = first,
TickSpacing = tickSpacing, TickSpacing = tickSpacing,
StartTime = t, StartTime = t,
IsStrong = IsStrong IsStrong = IsStrong,
Samples = Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToList()
}); });
first = false; first = false;

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
if (IsStrongBindable.Value != strongSamples.Any()) if (IsStrongBindable.Value != strongSamples.Any())
{ {
if (IsStrongBindable.Value) if (IsStrongBindable.Value)
Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH)); Samples.Add(GetSampleInfo(HitSampleInfo.HIT_FINISH));
else else
{ {
foreach (var sample in strongSamples) foreach (var sample in strongSamples)

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
public new BindableDouble TimeRange => base.TimeRange; public new BindableDouble TimeRange => base.TimeRange;
public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true); public readonly BindableBool LockPlayfieldAspectRange = new BindableBool(true);
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager; public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
@ -69,7 +69,9 @@ namespace osu.Game.Rulesets.Taiko.UI
const float scroll_rate = 10; const float scroll_rate = 10;
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space. // Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
float ratio = DrawHeight / 480; // Width is used because it defines how many notes fit on the playfield.
// We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default.
float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT);
TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate; TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
} }
@ -92,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.UI
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
{ {
LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect } LockPlayfieldAspectRange = { BindTarget = LockPlayfieldAspectRange }
}; };
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo); protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -17,12 +18,12 @@ namespace osu.Game.Rulesets.Taiko.UI
public void Play(HitType hitType) public void Play(HitType hitType)
{ {
var hitObject = GetMostValidObject(); var hitSample = GetMostValidObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (hitObject == null) if (hitSample == null)
return; return;
PlaySamples(new ISampleInfo[] { hitObject.SampleControlPoint.GetSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL) }); PlaySamples(new ISampleInfo[] { new HitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL, hitSample.Bank, volume: hitSample.Volume) });
} }
public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead"); public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");

View File

@ -11,9 +11,11 @@ namespace osu.Game.Rulesets.Taiko.UI
public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{ {
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768; private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
private const float default_aspect = 16f / 9f;
public readonly IBindable<bool> LockPlayfieldMaxAspect = new BindableBool(true); public const float MAXIMUM_ASPECT = 16f / 9f;
public const float MINIMUM_ASPECT = 5f / 4f;
public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
protected override void Update() protected override void Update()
{ {
@ -26,12 +28,22 @@ namespace osu.Game.Rulesets.Taiko.UI
// //
// As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit. // As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit.
// This is still a bit weird, because readability changes with window size, but it is what it is. // This is still a bit weird, because readability changes with window size, but it is what it is.
if (LockPlayfieldMaxAspect.Value && Parent.ChildSize.X / Parent.ChildSize.Y > default_aspect) if (LockPlayfieldAspectRange.Value)
height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect; {
float currentAspect = Parent.ChildSize.X / Parent.ChildSize.Y;
if (currentAspect > MAXIMUM_ASPECT)
height *= currentAspect / MAXIMUM_ASPECT;
else if (currentAspect < MINIMUM_ASPECT)
height *= currentAspect / MINIMUM_ASPECT;
}
// Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
height = Math.Min(height, 1f / 3f);
Height = height; Height = height;
// Position the taiko playfield exactly one playfield from the top of the screen. // Position the taiko playfield exactly one playfield from the top of the screen, if there is enough space for it.
// Note that the relative height cannot exceed one-third - if that limit is hit, the playfield will be exactly centered.
RelativePositionAxes = Axes.Y; RelativePositionAxes = Axes.Y;
Y = height; Y = height;
} }

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
@ -161,6 +160,51 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
[Test]
public void TestDecodeVideoWithLowercaseExtension()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var metadata = beatmap.Metadata;
Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
}
}
[Test]
public void TestDecodeVideoWithUppercaseExtension()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var metadata = beatmap.Metadata;
Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
}
}
[Test]
public void TestDecodeImageSpecifiedAsVideo()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("image-specified-as-video.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var metadata = beatmap.Metadata;
Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
}
}
[Test] [Test]
public void TestDecodeBeatmapTimingPoints() public void TestDecodeBeatmapTimingPoints()
{ {
@ -320,6 +364,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
{ {
var comboColors = decoder.Decode(stream).ComboColours; var comboColors = decoder.Decode(stream).ComboColours;
Debug.Assert(comboColors != null);
Color4[] expectedColors = Color4[] expectedColors =
{ {
new Color4(142, 199, 255, 255), new Color4(142, 199, 255, 255),
@ -330,7 +376,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
new Color4(255, 177, 140, 255), new Color4(255, 177, 140, 255),
new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored. new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored.
}; };
Assert.AreEqual(expectedColors.Length, comboColors?.Count); Assert.AreEqual(expectedColors.Length, comboColors.Count);
for (int i = 0; i < expectedColors.Length; i++) for (int i = 0; i < expectedColors.Length; i++)
Assert.AreEqual(expectedColors[i], comboColors[i]); Assert.AreEqual(expectedColors[i], comboColors[i]);
} }
@ -415,14 +461,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsNotNull(positionData); Assert.IsNotNull(positionData);
Assert.IsNotNull(curveData); Assert.IsNotNull(curveData);
Assert.AreEqual(new Vector2(192, 168), positionData.Position); Assert.AreEqual(new Vector2(192, 168), positionData!.Position);
Assert.AreEqual(956, hitObjects[0].StartTime); Assert.AreEqual(956, hitObjects[0].StartTime);
Assert.IsTrue(hitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL)); Assert.IsTrue(hitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL));
positionData = hitObjects[1] as IHasPosition; positionData = hitObjects[1] as IHasPosition;
Assert.IsNotNull(positionData); Assert.IsNotNull(positionData);
Assert.AreEqual(new Vector2(304, 56), positionData.Position); Assert.AreEqual(new Vector2(304, 56), positionData!.Position);
Assert.AreEqual(1285, hitObjects[1].StartTime); Assert.AreEqual(1285, hitObjects[1].StartTime);
Assert.IsTrue(hitObjects[1].Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)); Assert.IsTrue(hitObjects[1].Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP));
} }
@ -464,7 +510,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First()); Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First());
} }
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];
} }
[Test] [Test]
@ -482,7 +528,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("Gameplay/normal-hitnormal3", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); Assert.AreEqual("Gameplay/normal-hitnormal3", getTestableSampleInfo(hitObjects[2]).LookupNames.First());
} }
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];
} }
[Test] [Test]
@ -502,7 +548,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(70, getTestableSampleInfo(hitObjects[3]).Volume); Assert.AreEqual(70, getTestableSampleInfo(hitObjects[3]).Volume);
} }
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];
} }
[Test] [Test]
@ -578,8 +624,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestFallbackDecoderForCorruptedHeader() public void TestFallbackDecoderForCorruptedHeader()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("corrupted-header.osu")) using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -596,8 +642,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestFallbackDecoderForMissingHeader() public void TestFallbackDecoderForMissingHeader()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("missing-header.osu")) using (var resStream = TestResources.OpenResource("missing-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -614,8 +660,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestDecodeFileWithEmptyLinesAtStart() public void TestDecodeFileWithEmptyLinesAtStart()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("empty-lines-at-start.osu")) using (var resStream = TestResources.OpenResource("empty-lines-at-start.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -632,8 +678,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestDecodeFileWithEmptyLinesAndNoHeader() public void TestDecodeFileWithEmptyLinesAndNoHeader()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("empty-line-instead-of-header.osu")) using (var resStream = TestResources.OpenResource("empty-line-instead-of-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -650,8 +696,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestDecodeFileWithContentImmediatelyAfterHeader() public void TestDecodeFileWithContentImmediatelyAfterHeader()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("no-empty-line-after-header.osu")) using (var resStream = TestResources.OpenResource("no-empty-line-after-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -678,7 +724,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestAllowFallbackDecoderOverwrite() public void TestAllowFallbackDecoderOverwrite()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
using (var resStream = TestResources.OpenResource("corrupted-header.osu")) using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osuTK; using osuTK;
@ -30,35 +28,35 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(storyboard.HasDrawable); Assert.IsTrue(storyboard.HasDrawable);
Assert.AreEqual(6, storyboard.Layers.Count()); Assert.AreEqual(6, storyboard.Layers.Count());
StoryboardLayer background = storyboard.Layers.FirstOrDefault(l => l.Depth == 3); StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
Assert.IsNotNull(background); Assert.IsNotNull(background);
Assert.AreEqual(16, background.Elements.Count); Assert.AreEqual(16, background.Elements.Count);
Assert.IsTrue(background.VisibleWhenFailing); Assert.IsTrue(background.VisibleWhenFailing);
Assert.IsTrue(background.VisibleWhenPassing); Assert.IsTrue(background.VisibleWhenPassing);
Assert.AreEqual("Background", background.Name); Assert.AreEqual("Background", background.Name);
StoryboardLayer fail = storyboard.Layers.FirstOrDefault(l => l.Depth == 2); StoryboardLayer fail = storyboard.Layers.Single(l => l.Depth == 2);
Assert.IsNotNull(fail); Assert.IsNotNull(fail);
Assert.AreEqual(0, fail.Elements.Count); Assert.AreEqual(0, fail.Elements.Count);
Assert.IsTrue(fail.VisibleWhenFailing); Assert.IsTrue(fail.VisibleWhenFailing);
Assert.IsFalse(fail.VisibleWhenPassing); Assert.IsFalse(fail.VisibleWhenPassing);
Assert.AreEqual("Fail", fail.Name); Assert.AreEqual("Fail", fail.Name);
StoryboardLayer pass = storyboard.Layers.FirstOrDefault(l => l.Depth == 1); StoryboardLayer pass = storyboard.Layers.Single(l => l.Depth == 1);
Assert.IsNotNull(pass); Assert.IsNotNull(pass);
Assert.AreEqual(0, pass.Elements.Count); Assert.AreEqual(0, pass.Elements.Count);
Assert.IsFalse(pass.VisibleWhenFailing); Assert.IsFalse(pass.VisibleWhenFailing);
Assert.IsTrue(pass.VisibleWhenPassing); Assert.IsTrue(pass.VisibleWhenPassing);
Assert.AreEqual("Pass", pass.Name); Assert.AreEqual("Pass", pass.Name);
StoryboardLayer foreground = storyboard.Layers.FirstOrDefault(l => l.Depth == 0); StoryboardLayer foreground = storyboard.Layers.Single(l => l.Depth == 0);
Assert.IsNotNull(foreground); Assert.IsNotNull(foreground);
Assert.AreEqual(151, foreground.Elements.Count); Assert.AreEqual(151, foreground.Elements.Count);
Assert.IsTrue(foreground.VisibleWhenFailing); Assert.IsTrue(foreground.VisibleWhenFailing);
Assert.IsTrue(foreground.VisibleWhenPassing); Assert.IsTrue(foreground.VisibleWhenPassing);
Assert.AreEqual("Foreground", foreground.Name); Assert.AreEqual("Foreground", foreground.Name);
StoryboardLayer overlay = storyboard.Layers.FirstOrDefault(l => l.Depth == int.MinValue); StoryboardLayer overlay = storyboard.Layers.Single(l => l.Depth == int.MinValue);
Assert.IsNotNull(overlay); Assert.IsNotNull(overlay);
Assert.IsEmpty(overlay.Elements); Assert.IsEmpty(overlay.Elements);
Assert.IsTrue(overlay.VisibleWhenFailing); Assert.IsTrue(overlay.VisibleWhenFailing);
@ -76,7 +74,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var sprite = background.Elements.ElementAt(0) as StoryboardSprite; var sprite = background.Elements.ElementAt(0) as StoryboardSprite;
Assert.NotNull(sprite); Assert.NotNull(sprite);
Assert.IsTrue(sprite.HasCommands); Assert.IsTrue(sprite!.HasCommands);
Assert.AreEqual(new Vector2(320, 240), sprite.InitialPosition); Assert.AreEqual(new Vector2(320, 240), sprite.InitialPosition);
Assert.IsTrue(sprite.IsDrawable); Assert.IsTrue(sprite.IsDrawable);
Assert.AreEqual(Anchor.Centre, sprite.Origin); Assert.AreEqual(Anchor.Centre, sprite.Origin);
@ -97,6 +95,27 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
[Test]
public void TestLoopWithoutExplicitFadeOut()
{
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("animation-loop-no-explicit-end-time.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
Assert.AreEqual(1, background.Elements.Count);
Assert.AreEqual(2000, background.Elements[0].StartTime);
Assert.AreEqual(2000, (background.Elements[0] as StoryboardAnimation)?.EarliestTransformTime);
Assert.AreEqual(3000, (background.Elements[0] as StoryboardAnimation)?.GetEndTime());
Assert.AreEqual(12000, (background.Elements[0] as StoryboardAnimation)?.EndTimeForDisplay);
}
}
[Test] [Test]
public void TestCorrectAnimationStartTime() public void TestCorrectAnimationStartTime()
{ {
@ -171,6 +190,55 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
[Test]
public void TestDecodeVideoWithLowercaseExtension()
{
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
Assert.That(video.Elements.Count, Is.EqualTo(1));
Assert.AreEqual("Video.avi", ((StoryboardVideo)video.Elements[0]).Path);
}
}
[Test]
public void TestDecodeVideoWithUppercaseExtension()
{
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
Assert.That(video.Elements.Count, Is.EqualTo(1));
Assert.AreEqual("Video.AVI", ((StoryboardVideo)video.Elements[0]).Path);
}
}
[Test]
public void TestDecodeImageSpecifiedAsVideo()
{
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("image-specified-as-video.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
Assert.That(video.Elements.Count, Is.Zero);
}
}
[Test] [Test]
public void TestDecodeOutOfRangeLoopAnimationType() public void TestDecodeOutOfRangeLoopAnimationType()
{ {

View File

@ -37,45 +37,6 @@ namespace osu.Game.Tests.Editing.Checks
cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted }); cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });
} }
[Test]
public void TestNormalControlPointVolume()
{
var hitCircle = new HitCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertOk(new List<HitObject> { hitCircle });
}
[Test]
public void TestLowControlPointVolume()
{
var hitCircle = new HitCircle
{
StartTime = 1000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertLowVolume(new List<HitObject> { hitCircle });
}
[Test]
public void TestMutedControlPointVolume()
{
var hitCircle = new HitCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { hitCircle });
}
[Test] [Test]
public void TestNormalSampleVolume() public void TestNormalSampleVolume()
{ {
@ -122,7 +83,7 @@ namespace osu.Game.Tests.Editing.Checks
var sliderHead = new SliderHeadCircle var sliderHead = new SliderHeadCircle
{ {
StartTime = 0, StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
}; };
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
@ -135,7 +96,7 @@ namespace osu.Game.Tests.Editing.Checks
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500) var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
{ {
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
}; };
slider.ApplyDefaults(cpi, new BeatmapDifficulty()); slider.ApplyDefaults(cpi, new BeatmapDifficulty());
@ -155,13 +116,13 @@ namespace osu.Game.Tests.Editing.Checks
var sliderTick = new SliderTick var sliderTick = new SliderTick
{ {
StartTime = 250, StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") } Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick", volume: volume_regular) }
}; };
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500) var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
{ {
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail. Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) } // Applies to the tail.
}; };
slider.ApplyDefaults(cpi, new BeatmapDifficulty()); slider.ApplyDefaults(cpi, new BeatmapDifficulty());
@ -174,14 +135,14 @@ namespace osu.Game.Tests.Editing.Checks
var sliderHead = new SliderHeadCircle var sliderHead = new SliderHeadCircle
{ {
StartTime = 0, StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
}; };
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick var sliderTick = new SliderTick
{ {
StartTime = 250, StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") } Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick", volume: volume_regular) }
}; };
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
@ -194,59 +155,6 @@ namespace osu.Game.Tests.Editing.Checks
assertMutedPassive(new List<HitObject> { slider }); assertMutedPassive(new List<HitObject> { slider });
} }
[Test]
public void TestMutedControlPointVolumeSliderHead()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 2250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { slider });
}
[Test]
public void TestMutedControlPointVolumeSliderTail()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
// Ends after the 5% control point.
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMutedPassive(new List<HitObject> { slider });
}
private void assertOk(List<HitObject> hitObjects) private void assertOk(List<HitObject> hitObjects)
{ {
Assert.That(check.Run(getContext(hitObjects)), Is.Empty); Assert.That(check.Run(getContext(hitObjects)), Is.Empty);

View File

@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -74,12 +75,9 @@ namespace osu.Game.Tests.Editing
[TestCase(2)] [TestCase(2)]
public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier) public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier)
{ {
assertSnapDistance(100, new HitObject assertSnapDistance(100, new Slider
{ {
DifficultyControlPoint = new DifficultyControlPoint SliderVelocity = multiplier
{
SliderVelocity = multiplier
}
}, false); }, false);
} }
@ -87,12 +85,9 @@ namespace osu.Game.Tests.Editing
[TestCase(2)] [TestCase(2)]
public void TestSpeedMultiplierDoesChangeDistanceSnap(float multiplier) public void TestSpeedMultiplierDoesChangeDistanceSnap(float multiplier)
{ {
assertSnapDistance(100 * multiplier, new HitObject assertSnapDistance(100 * multiplier, new Slider
{ {
DifficultyControlPoint = new DifficultyControlPoint SliderVelocity = multiplier
{
SliderVelocity = multiplier
}
}, true); }, true);
} }
@ -114,12 +109,9 @@ namespace osu.Game.Tests.Editing
const float base_distance = 100; const float base_distance = 100;
const float slider_velocity = 1.2f; const float slider_velocity = 1.2f;
var referenceObject = new HitObject var referenceObject = new Slider
{ {
DifficultyControlPoint = new DifficultyControlPoint SliderVelocity = slider_velocity
{
SliderVelocity = slider_velocity
}
}; };
assertSnapDistance(base_distance * slider_velocity, referenceObject, true); assertSnapDistance(base_distance * slider_velocity, referenceObject, true);

View File

@ -1,11 +1,14 @@
#include "sh_Utils.h" #define HIGH_PRECISION_VERTEX
varying mediump vec2 v_TexCoord; #include "sh_Utils.h"
varying mediump vec4 v_TexRect; #include "sh_Masking.h"
layout(location = 2) in highp vec2 v_TexCoord;
layout(location = 0) out vec4 o_Colour;
void main(void) void main(void)
{ {
float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]); highp float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]);
gl_FragColor = hsv2rgb(vec4(hueValue, 1, 1, 1)); o_Colour = getRoundedColor(hsv2rgb(vec4(hueValue, 1, 1, 1)), v_TexCoord);
} }

View File

@ -1,31 +1,25 @@
#include "sh_Utils.h" layout(location = 0) in highp vec2 m_Position;
layout(location = 1) in lowp vec4 m_Colour;
layout(location = 2) in highp vec2 m_TexCoord;
layout(location = 3) in highp vec4 m_TexRect;
layout(location = 4) in mediump vec2 m_BlendRange;
attribute highp vec2 m_Position; layout(location = 0) out highp vec2 v_MaskingPosition;
attribute lowp vec4 m_Colour; layout(location = 1) out lowp vec4 v_Colour;
attribute mediump vec2 m_TexCoord; layout(location = 2) out highp vec2 v_TexCoord;
attribute mediump vec4 m_TexRect; layout(location = 3) out highp vec4 v_TexRect;
attribute mediump vec2 m_BlendRange; layout(location = 4) out mediump vec2 v_BlendRange;
varying highp vec2 v_MaskingPosition;
varying lowp vec4 v_Colour;
varying mediump vec2 v_TexCoord;
varying mediump vec4 v_TexRect;
varying mediump vec2 v_BlendRange;
uniform highp mat4 g_ProjMatrix;
uniform highp mat3 g_ToMaskingSpace;
void main(void) void main(void)
{ {
// Transform from screen space to masking space. // Transform from screen space to masking space.
highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0); highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
v_MaskingPosition = maskingPos.xy / maskingPos.z; v_MaskingPosition = maskingPos.xy / maskingPos.z;
v_Colour = m_Colour; v_Colour = m_Colour;
v_TexCoord = m_TexCoord; v_TexCoord = m_TexCoord;
v_TexRect = m_TexRect; v_TexRect = m_TexRect;
v_BlendRange = m_BlendRange; v_BlendRange = m_BlendRange;
gl_Position = gProjMatrix * vec4(m_Position, 1.0, 1.0); gl_Position = g_ProjMatrix * vec4(m_Position, 1.0, 1.0);
} }

View File

@ -0,0 +1,6 @@
[Events]
//Storyboard Layer 0 (Background)
Animation,Background,Centre,"img.jpg",320,240,2,150,LoopForever
F,0,2000,,0,1
L,2000,10
F,18,0,1000,1,0

View File

@ -0,0 +1,4 @@
osu file format v14
[Events]
Video,0,"BG.jpg",0,0

View File

@ -0,0 +1,5 @@
osu file format v14
[Events]
0,0,"BG.jpg",0,0
Video,0,"Video.avi",0,0

View File

@ -0,0 +1,5 @@
osu file format v14
[Events]
0,0,"BG.jpg",0,0
Video,0,"Video.AVI",0,0

View File

@ -164,7 +164,7 @@ namespace osu.Game.Tests.Rulesets
this.parentManager = parentManager; this.parentManager = parentManager;
} }
public override byte[] LoadRaw(string name) => parentManager.LoadRaw(name); public override byte[] GetRawData(string fileName) => parentManager.GetRawData(fileName);
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
@ -51,9 +49,13 @@ namespace osu.Game.Tests.Testing
[Test] [Test]
public void TestRetrieveShader() public void TestRetrieveShader()
{ {
AddAssert("ruleset shaders retrieved", () => AddStep("ruleset shaders retrieved without error", () =>
Dependencies.Get<ShaderManager>().LoadRaw(@"sh_TestVertex.vs") != null && {
Dependencies.Get<ShaderManager>().LoadRaw(@"sh_TestFragment.fs") != null); Dependencies.Get<ShaderManager>().GetRawData(@"sh_TestVertex.vs");
Dependencies.Get<ShaderManager>().GetRawData(@"sh_TestFragment.fs");
Dependencies.Get<ShaderManager>().Load(@"TestVertex", @"TestFragment");
Dependencies.Get<ShaderManager>().Load(VertexShaderDescriptor.TEXTURE_2, @"TestFragment");
});
} }
[Test] [Test]
@ -76,12 +78,12 @@ namespace osu.Game.Tests.Testing
} }
public override IResourceStore<byte[]> CreateResourceStore() => new NamespacedResourceStore<byte[]>(TestResources.GetStore(), @"Resources"); public override IResourceStore<byte[]> CreateResourceStore() => new NamespacedResourceStore<byte[]>(TestResources.GetStore(), @"Resources");
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new TestRulesetConfigManager(); public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TestRulesetConfigManager();
public override IEnumerable<Mod> GetModsFor(ModType type) => Array.Empty<Mod>(); public override IEnumerable<Mod> GetModsFor(ModType type) => Array.Empty<Mod>();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => null; public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => null!;
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null; public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!;
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null; public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!;
} }
private class TestRulesetConfigManager : IRulesetConfigManager private class TestRulesetConfigManager : IRulesetConfigManager

View File

@ -311,6 +311,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsDrawable => true; public bool IsDrawable => true;
public double StartTime => double.MinValue; public double StartTime => double.MinValue;
public double EndTime => double.MaxValue; public double EndTime => double.MaxValue;
public double EndTimeForDisplay => double.MaxValue;
public Drawable CreateDrawable() => new DrawableTestStoryboardElement(); public Drawable CreateDrawable() => new DrawableTestStoryboardElement();
} }

View File

@ -6,7 +6,6 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -95,10 +94,6 @@ namespace osu.Game.Tests.Visual.Editing
var path = slider.Path; var path = slider.Path;
return path.ControlPoints.Count == 2 && path.ControlPoints.SequenceEqual(addedObject.Path.ControlPoints); return path.ControlPoints.Count == 2 && path.ControlPoints.SequenceEqual(addedObject.Path.ControlPoints);
}); });
// see `HitObject.control_point_leniency`.
AddAssert("sample control point has correct time", () => Precision.AlmostEquals(slider.SampleControlPoint.Time, slider.GetEndTime(), 1));
AddAssert("difficulty control point has correct time", () => slider.DifficultyControlPoint.Time == slider.StartTime);
} }
[Test] [Test]

View File

@ -122,19 +122,9 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Beatmap has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500); AddAssert("Beatmap has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500);
// After placement these must be non-default as defaults are read-only.
AddAssert("Placed object has non-default control points", () =>
!ReferenceEquals(EditorBeatmap.HitObjects[0].SampleControlPoint, SampleControlPoint.DEFAULT) &&
!ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT));
ReloadEditorToSameBeatmap(); ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500); AddAssert("Beatmap still has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500);
// After placement these must be non-default as defaults are read-only.
AddAssert("Placed object still has non-default control points", () =>
!ReferenceEquals(EditorBeatmap.HitObjects[0].SampleControlPoint, SampleControlPoint.DEFAULT) &&
!ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT));
} }
[Test] [Test]

View File

@ -108,12 +108,16 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick()); AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
ExpandingToolboxContainer toolboxContainer = null!;
AddStep("move mouse to toolbox", () => InputManager.MoveMouseTo(toolboxContainer = hitObjectComposer.ChildrenOfType<ExpandingToolboxContainer>().First()));
AddUntilStep("toolbox is expanded", () => toolboxContainer.Expanded.Value);
AddUntilStep("wait for toolbox to expand", () => toolboxContainer.LatestTransformEndTime, () => Is.EqualTo(Time.Current));
AddStep("move mouse to overlapping toggle button", () => AddStep("move mouse to overlapping toggle button", () =>
{ {
var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad; var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad;
var button = hitObjectComposer var button = toolboxContainer.ChildrenOfType<DrawableTernaryButton>().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre));
.ChildrenOfType<ExpandingToolboxContainer>().First()
.ChildrenOfType<DrawableTernaryButton>().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre));
InputManager.MoveMouseTo(button); InputManager.MoveMouseTo(button);
}); });

View File

@ -8,10 +8,10 @@ using Humanizer;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
@ -61,10 +61,7 @@ namespace osu.Game.Tests.Visual.Editing
new PathControlPoint(new Vector2(100, 0)) new PathControlPoint(new Vector2(100, 0))
} }
}, },
DifficultyControlPoint = new DifficultyControlPoint SliderVelocity = 2
{
SliderVelocity = 2
}
}); });
}); });
} }
@ -100,8 +97,8 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("unify slider velocity", () => AddStep("unify slider velocity", () =>
{ {
foreach (var h in EditorBeatmap.HitObjects) foreach (var h in EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>())
h.DifficultyControlPoint.SliderVelocity = 1.5; h.SliderVelocity = 1.5;
}); });
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
@ -185,7 +182,7 @@ namespace osu.Game.Tests.Visual.Editing
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () => private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
{ {
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.DifficultyControlPoint.SliderVelocity == velocity; return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocity == velocity;
}); });
} }
} }

View File

@ -4,11 +4,12 @@
#nullable disable #nullable disable
using System.Linq; using System.Linq;
using System.Collections.Generic;
using Humanizer; using Humanizer;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -39,10 +40,9 @@ namespace osu.Game.Tests.Visual.Editing
{ {
StartTime = 0, StartTime = 0,
Position = (OsuPlayfield.BASE_SIZE - new Vector2(100, 0)) / 2, Position = (OsuPlayfield.BASE_SIZE - new Vector2(100, 0)) / 2,
SampleControlPoint = new SampleControlPoint Samples = new List<HitSampleInfo>
{ {
SampleBank = "normal", new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal", volume: 80)
SampleVolume = 80
} }
}); });
@ -50,10 +50,9 @@ namespace osu.Game.Tests.Visual.Editing
{ {
StartTime = 500, StartTime = 500,
Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2, Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2,
SampleControlPoint = new SampleControlPoint Samples = new List<HitSampleInfo>
{ {
SampleBank = "soft", new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft", volume: 60)
SampleVolume = 60
} }
}); });
}); });
@ -96,7 +95,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("unify sample volume", () => AddStep("unify sample volume", () =>
{ {
foreach (var h in EditorBeatmap.HitObjects) foreach (var h in EditorBeatmap.HitObjects)
h.SampleControlPoint.SampleVolume = 50; {
for (int i = 0; i < h.Samples.Count; i++)
{
h.Samples[i] = h.Samples[i].With(newVolume: 50);
}
}
}); });
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
@ -136,7 +140,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("unify sample bank", () => AddStep("unify sample bank", () =>
{ {
foreach (var h in EditorBeatmap.HitObjects) foreach (var h in EditorBeatmap.HitObjects)
h.SampleControlPoint.SampleBank = "soft"; {
for (int i = 0; i < h.Samples.Count; i++)
{
h.Samples[i] = h.Samples[i].With(newBank: "soft");
}
}
}); });
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
@ -248,7 +257,7 @@ namespace osu.Game.Tests.Visual.Editing
private void hitObjectHasSampleVolume(int objectIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} has volume {volume}", () => private void hitObjectHasSampleVolume(int objectIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} has volume {volume}", () =>
{ {
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.SampleControlPoint.SampleVolume == volume; return h.Samples.All(o => o.Volume == volume);
}); });
private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () => private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () =>
@ -265,7 +274,7 @@ namespace osu.Game.Tests.Visual.Editing
private void hitObjectHasSampleBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has bank {bank}", () => private void hitObjectHasSampleBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has bank {bank}", () =>
{ {
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.SampleControlPoint.SampleBank == bank; return h.Samples.All(o => o.Bank == bank);
}); });
} }
} }

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Game.Database;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneLocallyModifyingOnlineBeatmaps : EditorSavingTestScene
{
public override void SetUpSteps()
{
CreateInitialBeatmap = () =>
{
var importedSet = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).GetResultSafely();
return Game.BeatmapManager.GetWorkingBeatmap(importedSet!.Value.Beatmaps.First());
};
base.SetUpSteps();
}
[Test]
public void TestLocallyModifyingOnlineBeatmap()
{
AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0));
AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0));
SaveEditor();
ReloadEditorToSameBeatmap();
AddAssert("editor beatmap online ID reset", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.EqualTo(-1));
}
}
}

View File

@ -77,5 +77,39 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("object has non-zero duration", () => EditorBeatmap.HitObjects.OfType<IHasDuration>().Single().Duration > 0); AddAssert("object has non-zero duration", () => EditorBeatmap.HitObjects.OfType<IHasDuration>().Single().Duration > 0);
} }
[Test]
public void TestDisallowRepeatsOnZeroDurationObjects()
{
DragArea dragArea;
AddStep("add zero length slider", () =>
{
EditorBeatmap.Clear();
EditorBeatmap.Add(new Slider
{
Position = new Vector2(256, 256),
StartTime = 2700
});
});
AddStep("hold down drag bar", () =>
{
// distinguishes between the actual drag bar and its "underlay shadow".
dragArea = this.ChildrenOfType<DragArea>().Single(bar => bar.HandlePositionalInput);
InputManager.MoveMouseTo(dragArea);
InputManager.PressButton(MouseButton.Left);
});
AddStep("try to extend drag bar", () =>
{
var blueprint = this.ChildrenOfType<TimelineHitObjectBlueprint>().Single();
InputManager.MoveMouseTo(blueprint.SelectionQuad.TopLeft + new Vector2(100, 0));
});
AddStep("release button", () => InputManager.PressButton(MouseButton.Left));
AddAssert("object has zero repeats", () => EditorBeatmap.HitObjects.OfType<IHasRepeats>().Single().RepeatCount == 0);
}
} }
} }

View File

@ -35,14 +35,14 @@ namespace osu.Game.Tests.Visual.Gameplay
var referenceBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); var referenceBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 2));
seekTo(referenceBeatmap.Breaks[0].StartTime); seekTo(referenceBeatmap.Breaks[0].StartTime);
AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting.Value);
AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType<BreakInfo>().Single().AccuracyDisplay.Current.Value == 1); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType<BreakInfo>().Single().AccuracyDisplay.Current.Value == 1);
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0));
seekTo(referenceBeatmap.HitObjects[^1].GetEndTime()); seekTo(referenceBeatmap.HitObjects[^1].GetEndTime());
AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true); AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true);

View File

@ -31,11 +31,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addSeekStep(3000); addSeekStep(3000);
AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Select(kc => kc.CountPresses).Sum() == 15); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Select(kc => kc.CountPresses.Value).Sum() == 15);
AddStep("clear results", () => Player.Results.Clear()); AddStep("clear results", () => Player.Results.Clear());
addSeekStep(0); addSeekStep(0);
AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged));
AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0));
AddAssert("no results triggered", () => Player.Results.Count == 0); AddAssert("no results triggered", () => Player.Results.Count == 0);
} }

View File

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

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay
// best way to check without exposing. // best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter; private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First(); private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().Single();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -267,7 +267,7 @@ namespace osu.Game.Tests.Visual.Gameplay
hudOverlay = new HUDOverlay(null, Array.Empty<Mod>()); hudOverlay = new HUDOverlay(null, Array.Empty<Mod>());
// Add any key just to display the key counter visually. // Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space));
scoreProcessor.Combo.Value = 1; scoreProcessor.Combo.Value = 1;

View File

@ -8,6 +8,8 @@ using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -17,43 +19,63 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public TestSceneKeyCounter() public TestSceneKeyCounter()
{ {
KeyCounterKeyboard testCounter; KeyCounterDisplay defaultDisplay = new DefaultKeyCounterDisplay
KeyCounterDisplay kc = new KeyCounterDisplay
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Children = new KeyCounter[] Position = new Vector2(0, 72.7f)
{
testCounter = new KeyCounterKeyboard(Key.X),
new KeyCounterKeyboard(Key.X),
new KeyCounterMouse(MouseButton.Left),
new KeyCounterMouse(MouseButton.Right),
},
}; };
KeyCounterDisplay argonDisplay = new ArgonKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Position = new Vector2(0, -72.7f)
};
defaultDisplay.AddRange(new InputTrigger[]
{
new KeyCounterKeyboardTrigger(Key.X),
new KeyCounterKeyboardTrigger(Key.X),
new KeyCounterMouseTrigger(MouseButton.Left),
new KeyCounterMouseTrigger(MouseButton.Right),
});
argonDisplay.AddRange(new InputTrigger[]
{
new KeyCounterKeyboardTrigger(Key.X),
new KeyCounterKeyboardTrigger(Key.X),
new KeyCounterMouseTrigger(MouseButton.Left),
new KeyCounterMouseTrigger(MouseButton.Right),
});
var testCounter = (DefaultKeyCounter)defaultDisplay.Counters.First();
AddStep("Add random", () => AddStep("Add random", () =>
{ {
Key key = (Key)((int)Key.A + RNG.Next(26)); Key key = (Key)((int)Key.A + RNG.Next(26));
kc.Add(new KeyCounterKeyboard(key)); defaultDisplay.Add(new KeyCounterKeyboardTrigger(key));
argonDisplay.Add(new KeyCounterKeyboardTrigger(key));
}); });
Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key; Key testKey = ((KeyCounterKeyboardTrigger)defaultDisplay.Counters.First().Trigger).Key;
void addPressKeyStep() addPressKeyStep();
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 1);
addPressKeyStep();
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 2);
AddStep("Disable counting", () =>
{ {
AddStep($"Press {testKey} key", () => InputManager.Key(testKey)); argonDisplay.IsCounting.Value = false;
} defaultDisplay.IsCounting.Value = false;
});
addPressKeyStep();
AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses.Value == 2);
addPressKeyStep(); Add(defaultDisplay);
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1); Add(argonDisplay);
addPressKeyStep();
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2);
AddStep("Disable counting", () => testCounter.IsCounting = false);
addPressKeyStep();
AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses == 2);
Add(kc); void addPressKeyStep() => AddStep($"Press {testKey} key", () => InputManager.Key(testKey));
} }
} }
} }

View File

@ -45,6 +45,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved] [Resolved]
private SessionStatics sessionStatics { get; set; } private SessionStatics sessionStatics { get; set; }
[Resolved]
private OsuConfigManager config { get; set; }
[Cached(typeof(INotificationOverlay))] [Cached(typeof(INotificationOverlay))]
private readonly NotificationOverlay notificationOverlay; private readonly NotificationOverlay notificationOverlay;
@ -317,6 +320,7 @@ namespace osu.Game.Tests.Visual.Gameplay
saveVolumes(); saveVolumes();
setFullVolume(); setFullVolume();
AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("change epilepsy warning", () => epilepsyWarning = warning); AddStep("change epilepsy warning", () => epilepsyWarning = warning);
AddStep("load dummy beatmap", () => resetPlayer(false)); AddStep("load dummy beatmap", () => resetPlayer(false));
@ -333,12 +337,30 @@ namespace osu.Game.Tests.Visual.Gameplay
restoreVolumes(); restoreVolumes();
} }
[Test]
public void TestEpilepsyWarningWithDisabledStoryboard()
{
saveVolumes();
setFullVolume();
AddStep("disable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, false));
AddStep("change epilepsy warning", () => epilepsyWarning = true);
AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddUntilStep("epilepsy warning absent", () => getWarning() == null);
restoreVolumes();
}
[Test] [Test]
public void TestEpilepsyWarningEarlyExit() public void TestEpilepsyWarningEarlyExit()
{ {
saveVolumes(); saveVolumes();
setFullVolume(); setFullVolume();
AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set epilepsy warning", () => epilepsyWarning = true); AddStep("set epilepsy warning", () => epilepsyWarning = true);
AddStep("load dummy beatmap", () => resetPlayer(false)); AddStep("load dummy beatmap", () => resetPlayer(false));
@ -449,7 +471,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("click notification", () => notification.TriggerClick()); AddStep("click notification", () => notification.TriggerClick());
} }
private EpilepsyWarning getWarning() => loader.ChildrenOfType<EpilepsyWarning>().SingleOrDefault(); private EpilepsyWarning getWarning() => loader.ChildrenOfType<EpilepsyWarning>().SingleOrDefault(w => w.IsAlive);
private partial class TestPlayerLoader : PlayerLoader private partial class TestPlayerLoader : PlayerLoader
{ {

Some files were not shown because too many files have changed in this diff Show More