1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-21 08:52:54 +08:00

Merge remote-tracking branch 'upstream/master' into add-old-skin-to-osu-tests

This commit is contained in:
Dean Herbert 2019-12-17 13:56:01 +09:00
commit 6e1f10ad8d
73 changed files with 709 additions and 421 deletions

View File

@ -176,8 +176,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
#Style - C# 8 features #Style - C# 8 features
csharp_prefer_static_local_function = true:warning csharp_prefer_static_local_function = true:warning
csharp_prefer_simple_using_statement = true:silent csharp_prefer_simple_using_statement = true:silent
csharp_style_prefer_index_operator = false:none csharp_style_prefer_index_operator = true:warning
csharp_style_prefer_range_operator = false:none csharp_style_prefer_range_operator = true:warning
csharp_style_prefer_switch_expression = false:none csharp_style_prefer_switch_expression = false:none
#Supressing roslyn built-in analyzers #Supressing roslyn built-in analyzers

View File

@ -1,5 +1,5 @@
#addin "nuget:?package=CodeFileSanity&version=0.0.33" #addin "nuget:?package=CodeFileSanity&version=0.0.33"
#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2019.2.1" #addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2019.3.0"
#tool "nuget:?package=NVika.MSBuild&version=1.0.1" #tool "nuget:?package=NVika.MSBuild&version=1.0.1"
var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First(); var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First();

View File

@ -1,4 +1,9 @@
{ {
"sdk": {
"allowPrerelease": false,
"rollForward": "minor",
"version": "3.1.100"
},
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "2.0.24" "Microsoft.Build.Traversal": "2.0.24"
} }

View File

@ -53,7 +53,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.1215.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.1212.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2019.1215.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.Tests
protected override Player CreatePlayer(Ruleset ruleset) protected override Player CreatePlayer(Ruleset ruleset)
{ {
Mods.Value = Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray();
return base.CreatePlayer(ruleset); return base.CreatePlayer(ruleset);
} }
} }

View File

@ -151,7 +151,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private void addToPlayfield(DrawableCatchHitObject drawable) private void addToPlayfield(DrawableCatchHitObject drawable)
{ {
foreach (var mod in Mods.Value.OfType<IApplicableToDrawableHitObjects>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable }); mod.ApplyToDrawableHitObjects(new[] { drawable });
drawableRuleset.Playfield.Add(drawable); drawableRuleset.Playfield.Add(drawable);

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.Mods;
namespace osu.Game.Rulesets.Catch.Tests namespace osu.Game.Rulesets.Catch.Tests
@ -12,9 +13,10 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(CatchModHidden) }).ToList(); public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(CatchModHidden) }).ToList();
public TestSceneDrawableHitObjectsHidden() [SetUp]
public void SetUp() => Schedule(() =>
{ {
Mods.Value = new[] { new CatchModHidden() }; SelectedMods.Value = new[] { new CatchModHidden() };
} });
} }
} }

View File

@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.Catch.Objects
if (value != null) if (value != null)
{ {
path.ControlPoints.AddRange(value.ControlPoints); path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position.Value, c.Type.Value)));
path.ExpectedDistance.Value = value.ExpectedDistance.Value; path.ExpectedDistance.Value = value.ExpectedDistance.Value;
} }
} }

View File

@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
prevNoteTimes.RemoveAt(0); prevNoteTimes.RemoveAt(0);
prevNoteTimes.Add(newNoteTime); prevNoteTimes.Add(newNoteTime);
density = (prevNoteTimes[prevNoteTimes.Count - 1] - prevNoteTimes[0]) / prevNoteTimes.Count; density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
} }
private double lastTime; private double lastTime;

View File

@ -5,7 +5,7 @@ using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{ {
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current) protected override double StrainValueOf(DifficultyHitObject current)
{ {
var maniaCurrent = (ManiaDifficultyHitObject)current; var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = (maniaCurrent.BaseObject as HoldNote)?.EndTime ?? maniaCurrent.BaseObject.StartTime; var endTime = maniaCurrent.BaseObject.GetEndTime();
try try
{ {

View File

@ -4,7 +4,7 @@
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{ {
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current) protected override double StrainValueOf(DifficultyHitObject current)
{ {
var maniaCurrent = (ManiaDifficultyHitObject)current; var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = (maniaCurrent.BaseObject as HoldNote)?.EndTime ?? maniaCurrent.BaseObject.StartTime; var endTime = maniaCurrent.BaseObject.GetEndTime();
double holdFactor = 1.0; // Factor in case something else is held double holdFactor = 1.0; // Factor in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly

View File

@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests
var drawable = CreateDrawableHitCircle(circle, auto); var drawable = CreateDrawableHitCircle(circle, auto);
foreach (var mod in Mods.Value.OfType<IApplicableToDrawableHitObjects>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable }); mod.ApplyToDrawableHitObjects(new[] { drawable });
return drawable; return drawable;

View File

@ -14,9 +14,10 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList();
public TestSceneHitCircleHidden() [SetUp]
public void SetUp() => Schedule(() =>
{ {
Mods.Value = new[] { new OsuModHidden() }; SelectedMods.Value = new[] { new OsuModHidden() };
} });
} }
} }

View File

@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override Player CreatePlayer(Ruleset ruleset) protected override Player CreatePlayer(Ruleset ruleset)
{ {
Mods.Value = new Mod[] { new OsuModAutoplay(), new OsuModFlashlight(), }; SelectedMods.Value = new Mod[] { new OsuModAutoplay(), new OsuModFlashlight(), };
return base.CreatePlayer(ruleset); return base.CreatePlayer(ruleset);
} }

View File

@ -362,7 +362,7 @@ namespace osu.Game.Rulesets.Osu.Tests
var drawable = CreateDrawableSlider(slider); var drawable = CreateDrawableSlider(slider);
foreach (var mod in Mods.Value.OfType<IApplicableToDrawableHitObjects>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable }); mod.ApplyToDrawableHitObjects(new[] { drawable });
drawable.OnNewResult += onNewResult; drawable.OnNewResult += onNewResult;

View File

@ -14,9 +14,10 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList();
public TestSceneSliderHidden() [SetUp]
public void SetUp() => Schedule(() =>
{ {
Mods.Value = new[] { new OsuModHidden() }; SelectedMods.Value = new[] { new OsuModHidden() };
} });
} }
} }

View File

@ -286,11 +286,11 @@ namespace osu.Game.Rulesets.Osu.Tests
private bool assertGreatJudge() => judgementResults.Last().Type == HitResult.Great; private bool assertGreatJudge() => judgementResults.Last().Type == HitResult.Great;
private bool assertHeadMissTailTracked() => judgementResults[judgementResults.Count - 2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss; private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss;
private bool assertMidSliderJudgements() => judgementResults[judgementResults.Count - 2].Type == HitResult.Great; private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.Great;
private bool assertMidSliderJudgementFail() => judgementResults[judgementResults.Count - 2].Type == HitResult.Miss; private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.Miss;
private ScoreAccessibleReplayPlayer currentPlayer; private ScoreAccessibleReplayPlayer currentPlayer;

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Depth = depthIndex++ Depth = depthIndex++
}; };
foreach (var mod in Mods.Value.OfType<IApplicableToDrawableHitObjects>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable }); mod.ApplyToDrawableHitObjects(new[] { drawable });
Add(drawable); Add(drawable);

View File

@ -14,9 +14,10 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList();
public TestSceneSpinnerHidden() [SetUp]
public void SetUp() => Schedule(() =>
{ {
Mods.Value = new[] { new OsuModHidden() }; SelectedMods.Value = new[] { new OsuModHidden() };
} });
} }
} }

View File

@ -0,0 +1,75 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
/// <summary>
/// A visualisation of the line between two <see cref="PathControlPointPiece"/>s.
/// </summary>
public class PathControlPointConnectionPiece : CompositeDrawable
{
public PathControlPoint ControlPoint;
private readonly Path path;
private readonly Slider slider;
private IBindable<Vector2> sliderPosition;
private IBindable<int> pathVersion;
public PathControlPointConnectionPiece(Slider slider, PathControlPoint controlPoint)
{
this.slider = slider;
ControlPoint = controlPoint;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
InternalChild = path = new SmoothPath
{
Anchor = Anchor.Centre,
PathRadius = 1
};
}
protected override void LoadComplete()
{
base.LoadComplete();
sliderPosition = slider.PositionBindable.GetBoundCopy();
sliderPosition.BindValueChanged(_ => updateConnectingPath());
pathVersion = slider.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => updateConnectingPath());
updateConnectingPath();
}
/// <summary>
/// Updates the path connecting this control point to the next one.
/// </summary>
private void updateConnectingPath()
{
Position = slider.StackedPosition + ControlPoint.Position.Value;
path.ClearVertices();
int index = slider.Path.ControlPoints.IndexOf(ControlPoint) + 1;
if (index == 0 || index == slider.Path.ControlPoints.Count)
return;
path.AddVertex(Vector2.Zero);
path.AddVertex(slider.Path.ControlPoints[index].Position.Value - ControlPoint.Position.Value);
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
}
}
}

View File

@ -6,7 +6,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -19,6 +18,9 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
/// <summary>
/// A visualisation of a single <see cref="PathControlPoint"/> in a <see cref="Slider"/>.
/// </summary>
public class PathControlPointPiece : BlueprintPiece<Slider> public class PathControlPointPiece : BlueprintPiece<Slider>
{ {
public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection; public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
@ -28,7 +30,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public readonly PathControlPoint ControlPoint; public readonly PathControlPoint ControlPoint;
private readonly Slider slider; private readonly Slider slider;
private readonly Path path;
private readonly Container marker; private readonly Container marker;
private readonly Drawable markerRing; private readonly Drawable markerRing;
@ -39,12 +40,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
private IBindable<Vector2> sliderPosition; private IBindable<Vector2> sliderPosition;
private IBindable<int> pathVersion; private IBindable<Vector2> controlPointPosition;
public PathControlPointPiece(Slider slider, PathControlPoint controlPoint) public PathControlPointPiece(Slider slider, PathControlPoint controlPoint)
{ {
this.slider = slider; this.slider = slider;
ControlPoint = controlPoint; ControlPoint = controlPoint;
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -52,11 +52,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
path = new SmoothPath
{
Anchor = Anchor.Centre,
PathRadius = 1
},
marker = new Container marker = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -96,20 +91,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
base.LoadComplete(); base.LoadComplete();
sliderPosition = slider.PositionBindable.GetBoundCopy(); sliderPosition = slider.PositionBindable.GetBoundCopy();
sliderPosition.BindValueChanged(_ => updateDisplay()); sliderPosition.BindValueChanged(_ => updateMarkerDisplay());
pathVersion = slider.Path.Version.GetBoundCopy(); controlPointPosition = ControlPoint.Position.GetBoundCopy();
pathVersion.BindValueChanged(_ => updateDisplay()); controlPointPosition.BindValueChanged(_ => updateMarkerDisplay());
IsSelected.BindValueChanged(_ => updateMarkerDisplay()); IsSelected.BindValueChanged(_ => updateMarkerDisplay());
updateDisplay();
}
private void updateDisplay()
{
updateMarkerDisplay(); updateMarkerDisplay();
updateConnectingPath();
} }
// The connecting path is excluded from positional input // The connecting path is excluded from positional input
@ -189,26 +178,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
colour = Color4.White; colour = Color4.White;
marker.Colour = colour; marker.Colour = colour;
} }
/// <summary>
/// Updates the path connecting this control point to the previous one.
/// </summary>
private void updateConnectingPath()
{
path.ClearVertices();
int index = slider.Path.ControlPoints.IndexOf(ControlPoint);
if (index == -1)
return;
if (++index != slider.Path.ControlPoints.Count)
{
path.AddVertex(Vector2.Zero);
path.AddVertex(slider.Path.ControlPoints[index].Position.Value - ControlPoint.Position.Value);
}
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
}
} }
} }

View File

@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
internal readonly Container<PathControlPointPiece> Pieces; internal readonly Container<PathControlPointPiece> Pieces;
private readonly Container<PathControlPointConnectionPiece> connections;
private readonly Slider slider; private readonly Slider slider;
private readonly bool allowSelection; private readonly bool allowSelection;
@ -42,7 +44,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
InternalChild = Pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both }; InternalChildren = new Drawable[]
{
connections = new Container<PathControlPointConnectionPiece> { RelativeSizeAxes = Axes.Both },
Pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both }
};
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -62,19 +68,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
foreach (var point in controlPoints) foreach (var point in controlPoints)
{ {
var piece = new PathControlPointPiece(slider, point); Pieces.Add(new PathControlPointPiece(slider, point).With(d =>
{
if (allowSelection)
d.RequestSelection = selectPiece;
}));
if (allowSelection) connections.Add(new PathControlPointConnectionPiece(slider, point));
piece.RequestSelection = selectPiece;
Pieces.Add(piece);
} }
} }
private void removeControlPoints(IEnumerable<PathControlPoint> controlPoints) private void removeControlPoints(IEnumerable<PathControlPoint> controlPoints)
{ {
foreach (var point in controlPoints) foreach (var point in controlPoints)
{
Pieces.RemoveAll(p => p.ControlPoint == point); Pieces.RemoveAll(p => p.ControlPoint == point);
connections.RemoveAll(c => c.ControlPoint == point);
}
} }
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)

View File

@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override bool OnDoubleClick(DoubleClickEvent e) protected override bool OnDoubleClick(DoubleClickEvent e)
{ {
// Todo: This should all not occur on double click, but rather if the previous control point is hovered. // Todo: This should all not occur on double click, but rather if the previous control point is hovered.
segmentStart = HitObject.Path.ControlPoints[HitObject.Path.ControlPoints.Count - 1]; segmentStart = HitObject.Path.ControlPoints[^1];
segmentStart.Type.Value = PathType.Linear; segmentStart.Type.Value = PathType.Linear;
currentSegmentLength = 1; currentSegmentLength = 1;

View File

@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Objects
if (value != null) if (value != null)
{ {
path.ControlPoints.AddRange(value.ControlPoints); path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position.Value, c.Type.Value)));
path.ExpectedDistance.Value = value.ExpectedDistance.Value; path.ExpectedDistance.Value = value.ExpectedDistance.Value;
} }
} }

View File

@ -156,9 +156,9 @@ namespace osu.Game.Rulesets.Osu.Replays
// TODO: Shouldn't the spinner always spin in the same direction? // TODO: Shouldn't the spinner always spin in the same direction?
if (h is Spinner) if (h is Spinner)
{ {
calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[Frames.Count - 1]).Position, out startPosition, out spinnerDirection); calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection);
Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[Frames.Count - 1]).Position; Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[^1]).Position;
if (spinCentreOffset.Length > SPIN_RADIUS) if (spinCentreOffset.Length > SPIN_RADIUS)
{ {
@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Replays
private void moveToHitObject(OsuHitObject h, Vector2 targetPos, Easing easing) private void moveToHitObject(OsuHitObject h, Vector2 targetPos, Easing easing)
{ {
OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[Frames.Count - 1]; OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[^1];
// Wait until Auto could "see and react" to the next note. // Wait until Auto could "see and react" to the next note.
double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - reactionTime); double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - reactionTime);
@ -363,7 +363,7 @@ namespace osu.Game.Rulesets.Osu.Replays
} }
// We only want to let go of our button if we are at the end of the current replay. Otherwise something is still going on after us so we need to keep the button pressed! // We only want to let go of our button if we are at the end of the current replay. Otherwise something is still going on after us so we need to keep the button pressed!
if (Frames[Frames.Count - 1].Time <= endFrame.Time) if (Frames[^1].Time <= endFrame.Time)
AddFrameToReplay(endFrame); AddFrameToReplay(endFrame);
} }

View File

@ -28,18 +28,18 @@ namespace osu.Game.Rulesets.Osu.Skinning
InternalChildren = new[] InternalChildren = new[]
{ {
ExpandTarget = new NonPlayfieldSprite
{
Texture = skin.GetTexture("cursor"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new NonPlayfieldSprite new NonPlayfieldSprite
{ {
Texture = skin.GetTexture("cursormiddle"), Texture = skin.GetTexture("cursormiddle"),
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
ExpandTarget = new NonPlayfieldSprite
{
Texture = skin.GetTexture("cursor"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}; };
} }

View File

@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private void addPart(Vector2 screenSpacePosition) private void addPart(Vector2 screenSpacePosition)
{ {
parts[currentIndex].Position = screenSpacePosition; parts[currentIndex].Position = screenSpacePosition;
parts[currentIndex].Time = time; parts[currentIndex].Time = time + 1;
++parts[currentIndex].InvalidationID; ++parts[currentIndex].InvalidationID;
currentIndex = (currentIndex + 1) % max_sprites; currentIndex = (currentIndex + 1) % max_sprites;
@ -201,7 +201,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly TrailPart[] parts = new TrailPart[max_sprites]; private readonly TrailPart[] parts = new TrailPart[max_sprites];
private Vector2 size; private Vector2 size;
private readonly TrailBatch vertexBatch = new TrailBatch(max_sprites, 1); private readonly QuadBatch<TexturedTrailVertex> vertexBatch = new QuadBatch<TexturedTrailVertex>(max_sprites, 1);
public TrailDrawNode(CursorTrail source) public TrailDrawNode(CursorTrail source)
: base(source) : base(source)
@ -227,23 +227,52 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
shader.Bind(); shader.Bind();
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time); shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
for (int i = 0; i < parts.Length; ++i) texture.TextureGL.Bind();
RectangleF textureRect = texture.GetTextureRect();
foreach (var part in parts)
{ {
if (parts[i].InvalidationID == -1) if (part.InvalidationID == -1)
continue; continue;
vertexBatch.DrawTime = parts[i].Time; if (time - part.Time >= 1)
continue;
Vector2 pos = parts[i].Position; vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X - size.X / 2, part.Position.Y + size.Y / 2),
TexturePosition = textureRect.BottomLeft,
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
Time = part.Time
});
DrawQuad( vertexBatch.Add(new TexturedTrailVertex
texture, {
new Quad(pos.X - size.X / 2, pos.Y - size.Y / 2, size.X, size.Y), Position = new Vector2(part.Position.X + size.X / 2, part.Position.Y + size.Y / 2),
DrawColourInfo.Colour, TexturePosition = textureRect.BottomRight,
null, Colour = DrawColourInfo.Colour.BottomRight.Linear,
vertexBatch.AddAction); Time = part.Time
});
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X + size.X / 2, part.Position.Y - size.Y / 2),
TexturePosition = textureRect.TopRight,
Colour = DrawColourInfo.Colour.TopRight.Linear,
Time = part.Time
});
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X - size.X / 2, part.Position.Y - size.Y / 2),
TexturePosition = textureRect.TopLeft,
Colour = DrawColourInfo.Colour.TopLeft.Linear,
Time = part.Time
});
} }
vertexBatch.Draw();
shader.Unbind(); shader.Unbind();
} }
@ -253,25 +282,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Dispose(); vertexBatch.Dispose();
} }
// Todo: This shouldn't exist, but is currently used to reduce allocations by caching variable-capturing closures.
private class TrailBatch : QuadBatch<TexturedTrailVertex>
{
public new readonly Action<TexturedVertex2D> AddAction;
public float DrawTime;
public TrailBatch(int size, int maxBuffers)
: base(size, maxBuffers)
{
AddAction = v => Add(new TexturedTrailVertex
{
Position = v.Position,
TexturePosition = v.TexturePosition,
Time = DrawTime + 1,
Colour = v.Colour,
});
}
}
} }
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
protected override Player CreatePlayer(Ruleset ruleset) protected override Player CreatePlayer(Ruleset ruleset)
{ {
Mods.Value = Mods.Value.Concat(new[] { new TaikoModSuddenDeath() }).ToArray(); SelectedMods.Value = SelectedMods.Value.Concat(new[] { new TaikoModSuddenDeath() }).ToArray();
return new ScoreAccessiblePlayer(); return new ScoreAccessiblePlayer();
} }

View File

@ -3,7 +3,6 @@
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Rulesets.Taiko.Scoring;
@ -38,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
base.CreateNestedHitObjects(); base.CreateNestedHitObjects();
if (IsStrong) if (IsStrong)
AddNested(new StrongHitObject { StartTime = (this as IHasEndTime)?.EndTime ?? StartTime }); AddNested(new StrongHitObject { StartTime = this.GetEndTime() });
} }
public override Judgement CreateJudgement() => new TaikoJudgement(); public override Judgement CreateJudgement() => new TaikoJudgement();

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Beatmaps;
@ -39,9 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Replays
for (int i = 0; i < Beatmap.HitObjects.Count; i++) for (int i = 0; i < Beatmap.HitObjects.Count; i++)
{ {
TaikoHitObject h = Beatmap.HitObjects[i]; TaikoHitObject h = Beatmap.HitObjects[i];
double endTime = h.GetEndTime();
IHasEndTime endTimeData = h as IHasEndTime;
double endTime = endTimeData?.EndTime ?? h.StartTime;
switch (h) switch (h)
{ {

View File

@ -280,7 +280,7 @@ namespace osu.Game.Tests.Visual.Background
AddUntilStep("Song select has selection", () => songSelect.Carousel.SelectedBeatmap != null); AddUntilStep("Song select has selection", () => songSelect.Carousel.SelectedBeatmap != null);
AddStep("Set default user settings", () => AddStep("Set default user settings", () =>
{ {
Mods.Value = Mods.Value.Concat(new[] { new OsuModNoFail() }).ToArray(); SelectedMods.Value = SelectedMods.Value.Concat(new[] { new OsuModNoFail() }).ToArray();
songSelect.DimLevel.Value = 0.7f; songSelect.DimLevel.Value = 0.7f;
songSelect.BlurLevel.Value = 0.4f; songSelect.BlurLevel.Value = 0.4f;
}); });

View File

@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Player CreatePlayer(Ruleset ruleset) protected override Player CreatePlayer(Ruleset ruleset)
{ {
Mods.Value = Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray();
return new ScoreAccessiblePlayer(); return new ScoreAccessiblePlayer();
} }

View File

@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
protected override Player CreatePlayer(Ruleset ruleset) protected override Player CreatePlayer(Ruleset ruleset)
{ {
Mods.Value = Array.Empty<Mod>(); SelectedMods.Value = Array.Empty<Mod>();
return new FailPlayer(); return new FailPlayer();
} }

View File

@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
protected override Player CreatePlayer(Ruleset ruleset) protected override Player CreatePlayer(Ruleset ruleset)
{ {
Mods.Value = Array.Empty<Mod>(); SelectedMods.Value = Array.Empty<Mod>();
return new FailPlayer(); return new FailPlayer();
} }

View File

@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Player CreatePlayer(Ruleset ruleset) protected override Player CreatePlayer(Ruleset ruleset)
{ {
Mods.Value = Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray();
return new RulesetExposingPlayer(); return new RulesetExposingPlayer();
} }

View File

@ -0,0 +1,81 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneHUDOverlay : ManualInputManagerTestScene
{
private HUDOverlay hudOverlay;
private Drawable hideTarget => hudOverlay.KeyCounter; // best way of checking hideTargets without exposing.
[Resolved]
private OsuConfigManager config { get; set; }
[Test]
public void TestShownByDefault()
{
createNew();
AddAssert("showhud is set", () => hudOverlay.ShowHud.Value);
AddAssert("hidetarget is visible", () => hideTarget.IsPresent);
AddAssert("pause button is visible", () => hudOverlay.HoldToQuit.IsPresent);
}
[Test]
public void TestFadesInOnLoadComplete()
{
float? initialAlpha = null;
createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha);
AddUntilStep("wait for load", () => hudOverlay.IsAlive);
AddAssert("initial alpha was less than 1", () => initialAlpha != null && initialAlpha < 1);
}
[Test]
public void TestHideExternally()
{
createNew();
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent);
}
[Test]
public void TestExternalHideDoesntAffectConfig()
{
bool originalConfigValue = false;
createNew();
AddStep("get original config value", () => originalConfigValue = config.Get<bool>(OsuSetting.ShowInterface));
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
AddAssert("config unchanged", () => originalConfigValue == config.Get<bool>(OsuSetting.ShowInterface));
AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
AddAssert("config unchanged", () => originalConfigValue == config.Get<bool>(OsuSetting.ShowInterface));
}
private void createNew(Action<HUDOverlay> action = null)
{
AddStep("create overlay", () =>
{
Child = hudOverlay = new HUDOverlay(null, null, Array.Empty<Mod>());
action?.Invoke(hudOverlay);
});
}
}
}

View File

@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay
beforeLoadAction?.Invoke(); beforeLoadAction?.Invoke();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
foreach (var mod in Mods.Value.OfType<IApplicableToTrack>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToTrack>())
mod.ApplyToTrack(Beatmap.Value.Track); mod.ApplyToTrack(Beatmap.Value.Track);
InputManager.Child = container = new TestPlayerLoaderContainer( InputManager.Child = container = new TestPlayerLoaderContainer(
@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestEarlyExit() public void TestEarlyExit()
{ {
AddStep("load dummy beatmap", () => ResetPlayer(false, () => Mods.Value = new[] { new OsuModNightcore() })); AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() }));
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1); AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1);
AddStep("exit loader", () => loader.Exit()); AddStep("exit loader", () => loader.Exit());
@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.Gameplay
TestMod playerMod1 = null; TestMod playerMod1 = null;
TestMod playerMod2 = null; TestMod playerMod2 = null;
AddStep("load player", () => { ResetPlayer(true, () => Mods.Value = new[] { gameMod = new TestMod() }); }); AddStep("load player", () => { ResetPlayer(true, () => SelectedMods.Value = new[] { gameMod = new TestMod() }); });
AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen()); AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen());
AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre));

View File

@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("set second set", () => details.BeatmapSet = secondSet); AddStep("set second set", () => details.BeatmapSet = secondSet);
AddAssert("ratings set", () => details.Ratings.Metrics == secondSet.Metrics); AddAssert("ratings set", () => details.Ratings.Metrics == secondSet.Metrics);
BeatmapSetInfo createSet() => new BeatmapSetInfo static BeatmapSetInfo createSet() => new BeatmapSetInfo
{ {
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).Select(_ => RNG.Next(10)).ToArray() }, Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).Select(_ => RNG.Next(10)).ToArray() },
Beatmaps = new List<BeatmapInfo> Beatmaps = new List<BeatmapInfo>

View File

@ -256,17 +256,17 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("change ruleset", () => AddStep("change ruleset", () =>
{ {
Mods.ValueChanged += onModChange; SelectedMods.ValueChanged += onModChange;
songSelect.Ruleset.ValueChanged += onRulesetChange; songSelect.Ruleset.ValueChanged += onRulesetChange;
Ruleset.Value = new TaikoRuleset().RulesetInfo; Ruleset.Value = new TaikoRuleset().RulesetInfo;
Mods.ValueChanged -= onModChange; SelectedMods.ValueChanged -= onModChange;
songSelect.Ruleset.ValueChanged -= onRulesetChange; songSelect.Ruleset.ValueChanged -= onRulesetChange;
}); });
AddAssert("mods changed before ruleset", () => modChangeIndex < rulesetChangeIndex); AddAssert("mods changed before ruleset", () => modChangeIndex < rulesetChangeIndex);
AddAssert("empty mods", () => !Mods.Value.Any()); AddAssert("empty mods", () => !SelectedMods.Value.Any());
void onModChange(ValueChangedEvent<IReadOnlyList<Mod>> e) => modChangeIndex = actionIndex++; void onModChange(ValueChangedEvent<IReadOnlyList<Mod>> e) => modChangeIndex = actionIndex++;
void onRulesetChange(ValueChangedEvent<RulesetInfo> e) => rulesetChangeIndex = actionIndex++; void onRulesetChange(ValueChangedEvent<RulesetInfo> e) => rulesetChangeIndex = actionIndex++;
@ -275,7 +275,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[Test] [Test]
public void TestModsRetainedBetweenSongSelect() public void TestModsRetainedBetweenSongSelect()
{ {
AddAssert("empty mods", () => !Mods.Value.Any()); AddAssert("empty mods", () => !SelectedMods.Value.Any());
createSongSelect(); createSongSelect();
@ -285,7 +285,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect(); createSongSelect();
AddAssert("mods retained", () => Mods.Value.Any()); AddAssert("mods retained", () => SelectedMods.Value.Any());
} }
[Test] [Test]
@ -332,7 +332,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private void checkMusicPlaying(bool playing) => private void checkMusicPlaying(bool playing) =>
AddUntilStep($"music {(playing ? "" : "not ")}playing", () => music.IsPlaying == playing); AddUntilStep($"music {(playing ? "" : "not ")}playing", () => music.IsPlaying == playing);
private void changeMods(params Mod[] mods) => AddStep($"change mods to {string.Join(", ", mods.Select(m => m.Acronym))}", () => Mods.Value = mods); private void changeMods(params Mod[] mods) => AddStep($"change mods to {string.Join(", ", mods.Select(m => m.Acronym))}", () => SelectedMods.Value = mods);
private void changeRuleset(int id) => AddStep($"change ruleset to {id}", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == id)); private void changeRuleset(int id) => AddStep($"change ruleset to {id}", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == id));

View File

@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private TimingControlPoint getNextTimingPoint(TimingControlPoint current) private TimingControlPoint getNextTimingPoint(TimingControlPoint current)
{ {
if (timingPoints[timingPoints.Count - 1] == current) if (timingPoints[^1] == current)
return current; return current;
int index = timingPoints.IndexOf(current); // -1 means that this is a "default beat" int index = timingPoints.IndexOf(current); // -1 means that this is a "default beat"
@ -169,7 +169,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
if (timingPoints.Count == 0) return 0; if (timingPoints.Count == 0) return 0;
if (timingPoints[timingPoints.Count - 1] == current) if (timingPoints[^1] == current)
return (int)Math.Ceiling((Beatmap.Value.Track.Length - current.Time) / current.BeatLength); return (int)Math.Ceiling((Beatmap.Value.Track.Length - current.Time) / current.BeatLength);
return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength);

View File

@ -8,13 +8,16 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Mods.Sections; using osu.Game.Overlays.Mods.Sections;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
@ -48,42 +51,48 @@ namespace osu.Game.Tests.Visual.UserInterface
private void load(RulesetStore rulesets) private void load(RulesetStore rulesets)
{ {
this.rulesets = rulesets; this.rulesets = rulesets;
}
Add(modSelect = new TestModSelectOverlay [SetUp]
public void SetUp() => Schedule(() =>
{
Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.X, modSelect = new TestModSelectOverlay
Origin = Anchor.BottomCentre, {
Anchor = Anchor.BottomCentre, RelativeSizeAxes = Axes.X,
}); Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
},
Add(modDisplay = new ModDisplay modDisplay = new ModDisplay
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Position = new Vector2(0, 25), Position = new Vector2(0, 25),
}); Current = { BindTarget = modSelect.SelectedMods }
}
};
});
modDisplay.Current.UnbindBindings(); [SetUpSteps]
modDisplay.Current.BindTo(modSelect.SelectedMods); public void SetUpSteps()
{
AddStep("Show", modSelect.Show); AddStep("show", () => modSelect.Show());
AddStep("Toggle", modSelect.ToggleVisibility);
AddStep("Toggle", modSelect.ToggleVisibility);
} }
[Test] [Test]
public void TestOsuMods() public void TestOsuMods()
{ {
var ruleset = rulesets.AvailableRulesets.First(r => r.ID == 0); changeRuleset(0);
changeRuleset(ruleset);
var instance = ruleset.CreateInstance(); var osu = new OsuRuleset();
var easierMods = instance.GetModsFor(ModType.DifficultyReduction); var easierMods = osu.GetModsFor(ModType.DifficultyReduction);
var harderMods = instance.GetModsFor(ModType.DifficultyIncrease); var harderMods = osu.GetModsFor(ModType.DifficultyIncrease);
var noFailMod = easierMods.FirstOrDefault(m => m is OsuModNoFail); var noFailMod = osu.GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden); var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden);
var doubleTimeMod = harderMods.OfType<MultiMod>().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime)); var doubleTimeMod = harderMods.OfType<MultiMod>().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime));
@ -97,8 +106,8 @@ namespace osu.Game.Tests.Visual.UserInterface
testMultiMod(doubleTimeMod); testMultiMod(doubleTimeMod);
testIncompatibleMods(easy, hardRock); testIncompatibleMods(easy, hardRock);
testDeselectAll(easierMods.Where(m => !(m is MultiMod))); testDeselectAll(easierMods.Where(m => !(m is MultiMod)));
testMultiplierTextColour(noFailMod, modSelect.LowMultiplierColour); testMultiplierTextColour(noFailMod, () => modSelect.LowMultiplierColour);
testMultiplierTextColour(hiddenMod, modSelect.HighMultiplierColour); testMultiplierTextColour(hiddenMod, () => modSelect.HighMultiplierColour);
testUnimplementedMod(spunOutMod); testUnimplementedMod(spunOutMod);
} }
@ -106,37 +115,31 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test] [Test]
public void TestManiaMods() public void TestManiaMods()
{ {
var ruleset = rulesets.AvailableRulesets.First(r => r.ID == 3); changeRuleset(3);
changeRuleset(ruleset);
testRankedText(ruleset.CreateInstance().GetModsFor(ModType.Conversion).First(m => m is ManiaModRandom)); testRankedText(new ManiaRuleset().GetModsFor(ModType.Conversion).First(m => m is ManiaModRandom));
} }
[Test] [Test]
public void TestRulesetChanges() public void TestRulesetChanges()
{ {
var rulesetOsu = rulesets.AvailableRulesets.First(r => r.ID == 0); changeRuleset(0);
var rulesetMania = rulesets.AvailableRulesets.First(r => r.ID == 3);
changeRuleset(null); var noFailMod = new OsuRuleset().GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
var instance = rulesetOsu.CreateInstance(); AddStep("set mods externally", () => { SelectedMods.Value = new[] { noFailMod }; });
var easierMods = instance.GetModsFor(ModType.DifficultyReduction);
var noFailMod = easierMods.FirstOrDefault(m => m is OsuModNoFail);
AddStep("set mods externally", () => { modDisplay.Current.Value = new[] { noFailMod }; }); changeRuleset(0);
changeRuleset(rulesetOsu);
AddAssert("ensure mods still selected", () => modDisplay.Current.Value.Single(m => m is OsuModNoFail) != null); AddAssert("ensure mods still selected", () => modDisplay.Current.Value.Single(m => m is OsuModNoFail) != null);
changeRuleset(rulesetMania); changeRuleset(3);
AddAssert("ensure mods not selected", () => !modDisplay.Current.Value.Any(m => m is OsuModNoFail)); AddAssert("ensure mods not selected", () => modDisplay.Current.Value.Count == 0);
changeRuleset(rulesetOsu); changeRuleset(0);
AddAssert("ensure mods not selected", () => !modDisplay.Current.Value.Any()); AddAssert("ensure mods not selected", () => modDisplay.Current.Value.Count == 0);
} }
private void testSingleMod(Mod mod) private void testSingleMod(Mod mod)
@ -198,19 +201,19 @@ namespace osu.Game.Tests.Visual.UserInterface
selectNext(mod); selectNext(mod);
AddAssert("check for any selection", () => modSelect.SelectedMods.Value.Any()); AddAssert("check for any selection", () => modSelect.SelectedMods.Value.Any());
AddStep("deselect all", modSelect.DeselectAllButton.Action.Invoke); AddStep("deselect all", () => modSelect.DeselectAllButton.Action.Invoke());
AddAssert("check for no selection", () => !modSelect.SelectedMods.Value.Any()); AddAssert("check for no selection", () => !modSelect.SelectedMods.Value.Any());
} }
private void testMultiplierTextColour(Mod mod, Color4 colour) private void testMultiplierTextColour(Mod mod, Func<Color4> getCorrectColour)
{ {
checkLabelColor(Color4.White); checkLabelColor(() => Color4.White);
selectNext(mod); selectNext(mod);
AddWaitStep("wait for changing colour", 1); AddWaitStep("wait for changing colour", 1);
checkLabelColor(colour); checkLabelColor(getCorrectColour);
selectPrevious(mod); selectPrevious(mod);
AddWaitStep("wait for changing colour", 1); AddWaitStep("wait for changing colour", 1);
checkLabelColor(Color4.White); checkLabelColor(() => Color4.White);
} }
private void testRankedText(Mod mod) private void testRankedText(Mod mod)
@ -235,9 +238,9 @@ namespace osu.Game.Tests.Visual.UserInterface
}); });
} }
private void changeRuleset(RulesetInfo ruleset) private void changeRuleset(int? id)
{ {
AddStep($"change ruleset to {ruleset}", () => { Ruleset.Value = ruleset; }); AddStep($"change ruleset to {(id?.ToString() ?? "none")}", () => { Ruleset.Value = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == id); });
waitForLoad(); waitForLoad();
} }
@ -253,7 +256,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}); });
} }
private void checkLabelColor(Color4 color) => AddAssert("check label has expected colour", () => modSelect.MultiplierLabel.Colour.AverageColour == color); private void checkLabelColor(Func<Color4> getColour) => AddAssert("check label has expected colour", () => modSelect.MultiplierLabel.Colour.AverageColour == getColour());
private class TestModSelectOverlay : ModSelectOverlay private class TestModSelectOverlay : ModSelectOverlay
{ {

View File

@ -2,15 +2,20 @@
// 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.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
@ -18,28 +23,51 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
private TestModSelectOverlay modSelect; private TestModSelectOverlay modSelect;
[BackgroundDependencyLoader] private readonly Mod testCustomisableMod = new TestModCustomisable1();
private void load()
[Test]
public void TestButtonShowsOnCustomisableMod()
{ {
Add(modSelect = new TestModSelectOverlay createModSelect();
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
});
var testMod = new TestModCustomisable1(); AddStep("open", () => modSelect.Show());
AddStep("open", modSelect.Show);
AddAssert("button disabled", () => !modSelect.CustomiseButton.Enabled.Value); AddAssert("button disabled", () => !modSelect.CustomiseButton.Enabled.Value);
AddUntilStep("wait for button load", () => modSelect.ButtonsLoaded); AddUntilStep("wait for button load", () => modSelect.ButtonsLoaded);
AddStep("select mod", () => modSelect.SelectMod(testMod)); AddStep("select mod", () => modSelect.SelectMod(testCustomisableMod));
AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value); AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value);
AddStep("open Customisation", () => modSelect.CustomiseButton.Click()); AddStep("open Customisation", () => modSelect.CustomiseButton.Click());
AddStep("deselect mod", () => modSelect.SelectMod(testMod)); AddStep("deselect mod", () => modSelect.SelectMod(testCustomisableMod));
AddAssert("controls hidden", () => modSelect.ModSettingsContainer.Alpha == 0); AddAssert("controls hidden", () => modSelect.ModSettingsContainer.Alpha == 0);
} }
[Test]
public void TestButtonShowsOnModAlreadyAdded()
{
AddStep("set active mods", () => SelectedMods.Value = new List<Mod> { testCustomisableMod });
createModSelect();
AddAssert("mods still active", () => SelectedMods.Value.Count == 1);
AddStep("open", () => modSelect.Show());
AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value);
}
private void createModSelect()
{
AddStep("create mod select", () =>
{
Ruleset.Value = new TestRulesetInfo();
Child = modSelect = new TestModSelectOverlay
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
};
});
}
private class TestModSelectOverlay : ModSelectOverlay private class TestModSelectOverlay : ModSelectOverlay
{ {
public new Container ModSettingsContainer => base.ModSettingsContainer; public new Container ModSettingsContainer => base.ModSettingsContainer;
@ -50,24 +78,41 @@ namespace osu.Game.Tests.Visual.UserInterface
public void SelectMod(Mod mod) => public void SelectMod(Mod mod) =>
ModSectionsContainer.Children.Single(s => s.ModType == mod.Type) ModSectionsContainer.Children.Single(s => s.ModType == mod.Type)
.ButtonsContainer.OfType<ModButton>().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1); .ButtonsContainer.OfType<ModButton>().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1);
}
protected override void LoadComplete() public class TestRulesetInfo : RulesetInfo
{
public override Ruleset CreateInstance() => new TestCustomisableModRuleset();
public TestRulesetInfo()
{ {
base.LoadComplete(); Available = true;
}
foreach (var section in ModSectionsContainer) public class TestCustomisableModRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type)
{ {
if (section.ModType == ModType.Conversion) if (type == ModType.Conversion)
{ {
section.Mods = new Mod[] return new Mod[]
{ {
new TestModCustomisable1(), new TestModCustomisable1(),
new TestModCustomisable2() new TestModCustomisable2()
}; };
} }
else
section.Mods = Array.Empty<Mod>(); return Array.Empty<Mod>();
} }
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => throw new NotImplementedException();
public override string Description { get; } = "test";
public override string ShortName { get; } = "tst";
} }
} }

View File

@ -195,8 +195,8 @@ namespace osu.Game.Beatmaps.ControlPoints
if (time < list[0].Time) if (time < list[0].Time)
return null; return null;
if (time >= list[list.Count - 1].Time) if (time >= list[^1].Time)
return list[list.Count - 1]; return list[^1];
int l = 0; int l = 0;
int r = list.Count - 2; int r = list.Count - 2;

View File

@ -34,7 +34,7 @@ namespace osu.Game.Beatmaps.Formats
if (line.StartsWith(@"[", StringComparison.Ordinal) && line.EndsWith(@"]", StringComparison.Ordinal)) if (line.StartsWith(@"[", StringComparison.Ordinal) && line.EndsWith(@"]", StringComparison.Ordinal))
{ {
if (!Enum.TryParse(line.Substring(1, line.Length - 2), out section)) if (!Enum.TryParse(line[1..^1], out section))
{ {
Logger.Log($"Unknown section \"{line}\" in \"{output}\""); Logger.Log($"Unknown section \"{line}\" in \"{output}\"");
section = Section.None; section = Section.None;

View File

@ -37,7 +37,7 @@ namespace osu.Game.Graphics.Containers
foreach (var link in links) foreach (var link in links)
{ {
AddText(text.Substring(previousLinkEnd, link.Index - previousLinkEnd)); AddText(text[previousLinkEnd..link.Index]);
AddLink(text.Substring(link.Index, link.Length), link.Action, link.Argument ?? link.Url); AddLink(text.Substring(link.Index, link.Length), link.Action, link.Argument ?? link.Url);
previousLinkEnd = link.Index + link.Length; previousLinkEnd = link.Index + link.Length;
} }

View File

@ -83,6 +83,15 @@ namespace osu.Game.Graphics.Containers
return base.OnDragEnd(e); return base.OnDragEnd(e);
} }
protected override bool OnScroll(ScrollEvent e)
{
// allow for controlling volume when alt is held.
// mostly for compatibility with osu-stable.
if (e.AltPressed) return false;
return base.OnScroll(e);
}
protected override ScrollbarContainer CreateScrollbar(Direction direction) => new OsuScrollbar(direction); protected override ScrollbarContainer CreateScrollbar(Direction direction) => new OsuScrollbar(direction);
protected class OsuScrollbar : ScrollbarContainer protected class OsuScrollbar : ScrollbarContainer

View File

@ -80,8 +80,13 @@ namespace osu.Game
// todo: move this to SongSelect once Screen has the ability to unsuspend. // todo: move this to SongSelect once Screen has the ability to unsuspend.
[Cached] [Cached]
[Cached(Type = typeof(IBindable<IReadOnlyList<Mod>>))] [Cached(typeof(IBindable<IReadOnlyList<Mod>>))]
protected readonly Bindable<IReadOnlyList<Mod>> Mods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>()); protected readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
/// <summary>
/// Mods available for the current <see cref="Ruleset"/>.
/// </summary>
public readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> AvailableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
protected Bindable<WorkingBeatmap> Beatmap { get; private set; } // cached via load() method protected Bindable<WorkingBeatmap> Beatmap { get; private set; } // cached via load() method
@ -233,6 +238,23 @@ namespace osu.Game
PreviewTrackManager previewTrackManager; PreviewTrackManager previewTrackManager;
dependencies.Cache(previewTrackManager = new PreviewTrackManager()); dependencies.Cache(previewTrackManager = new PreviewTrackManager());
Add(previewTrackManager); Add(previewTrackManager);
Ruleset.BindValueChanged(onRulesetChanged);
}
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> r)
{
var dict = new Dictionary<ModType, IReadOnlyList<Mod>>();
if (r.NewValue?.Available == true)
{
foreach (ModType type in Enum.GetValues(typeof(ModType)))
dict[type] = r.NewValue.CreateInstance().GetModsFor(type).ToList();
}
if (!SelectedMods.Disabled)
SelectedMods.Value = Array.Empty<Mod>();
AvailableMods.Value = dict;
} }
protected virtual Container CreateScalingContainer() => new DrawSizePreservingFillContainer(); protected virtual Container CreateScalingContainer() => new DrawSizePreservingFillContainer();

View File

@ -1,10 +1,10 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -12,14 +12,13 @@ using osuTK;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public class ModControlSection : Container public class ModControlSection : CompositeDrawable
{ {
protected FillFlowContainer FlowContent; protected FillFlowContainer FlowContent;
protected override Container<Drawable> Content => FlowContent;
public readonly Mod Mod; public readonly Mod Mod;
public ModControlSection(Mod mod) public ModControlSection(Mod mod, IEnumerable<Drawable> modControls)
{ {
Mod = mod; Mod = mod;
@ -33,9 +32,8 @@ namespace osu.Game.Overlays.Mods
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
ChildrenEnumerable = modControls
}; };
AddRange(Mod.CreateSettingsControls());
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -20,7 +20,6 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods.Sections; using osu.Game.Overlays.Mods.Sections;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens; using osu.Game.Screens;
using osuTK; using osuTK;
@ -50,7 +49,7 @@ namespace osu.Game.Overlays.Mods
protected readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>()); protected readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
protected readonly IBindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>(); private Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods;
protected Color4 LowMultiplierColour; protected Color4 LowMultiplierColour;
protected Color4 HighMultiplierColour; protected Color4 HighMultiplierColour;
@ -322,14 +321,14 @@ namespace osu.Game.Overlays.Mods
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuColour colours, IBindable<RulesetInfo> ruleset, AudioManager audio, Bindable<IReadOnlyList<Mod>> mods) private void load(OsuColour colours, AudioManager audio, Bindable<IReadOnlyList<Mod>> selectedMods, OsuGameBase osu)
{ {
LowMultiplierColour = colours.Red; LowMultiplierColour = colours.Red;
HighMultiplierColour = colours.Green; HighMultiplierColour = colours.Green;
UnrankedLabel.Colour = colours.Blue; UnrankedLabel.Colour = colours.Blue;
Ruleset.BindTo(ruleset); availableMods = osu.AvailableMods.GetBoundCopy();
if (mods != null) SelectedMods.BindTo(mods); SelectedMods.BindTo(selectedMods);
sampleOn = audio.Samples.Get(@"UI/check-on"); sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off"); sampleOff = audio.Samples.Get(@"UI/check-off");
@ -360,7 +359,7 @@ namespace osu.Game.Overlays.Mods
{ {
base.LoadComplete(); base.LoadComplete();
Ruleset.BindValueChanged(rulesetChanged, true); availableMods.BindValueChanged(availableModsChanged, true);
SelectedMods.BindValueChanged(selectedModsChanged, true); SelectedMods.BindValueChanged(selectedModsChanged, true);
} }
@ -410,22 +409,12 @@ namespace osu.Game.Overlays.Mods
return base.OnKeyDown(e); return base.OnKeyDown(e);
} }
private void rulesetChanged(ValueChangedEvent<RulesetInfo> e) private void availableModsChanged(ValueChangedEvent<Dictionary<ModType, IReadOnlyList<Mod>>> mods)
{ {
if (e.NewValue == null) return; if (mods.NewValue == null) return;
var instance = e.NewValue.CreateInstance();
foreach (var section in ModSectionsContainer.Children) foreach (var section in ModSectionsContainer.Children)
section.Mods = instance.GetModsFor(section.ModType); section.Mods = mods.NewValue[section.ModType];
// attempt to re-select any already selected mods.
// this may be the first time we are receiving the ruleset, in which case they will still match.
selectedModsChanged(new ValueChangedEvent<IReadOnlyList<Mod>>(SelectedMods.Value, SelectedMods.Value));
// write the mods back to the SelectedMods bindable in the case a change was not applicable.
// this generally isn't required as the previous line will perform deselection; just here for safety.
refreshSelectedMods();
} }
private void selectedModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods) private void selectedModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
@ -462,17 +451,17 @@ namespace osu.Game.Overlays.Mods
private void updateModSettings(ValueChangedEvent<IReadOnlyList<Mod>> selectedMods) private void updateModSettings(ValueChangedEvent<IReadOnlyList<Mod>> selectedMods)
{ {
foreach (var added in selectedMods.NewValue.Except(selectedMods.OldValue)) ModSettingsContent.Clear();
foreach (var mod in selectedMods.NewValue)
{ {
var controls = added.CreateSettingsControls().ToList(); var settings = mod.CreateSettingsControls().ToList();
if (controls.Count > 0) if (settings.Count > 0)
ModSettingsContent.Add(new ModControlSection(added) { Children = controls }); ModSettingsContent.Add(new ModControlSection(mod, settings));
} }
foreach (var removed in selectedMods.OldValue.Except(selectedMods.NewValue)) bool hasSettings = ModSettingsContent.Count > 0;
ModSettingsContent.RemoveAll(section => section.Mod == removed);
bool hasSettings = ModSettingsContent.Children.Count > 0;
CustomiseButton.Enabled.Value = hasSettings; CustomiseButton.Enabled.Value = hasSettings;
if (!hasSettings) if (!hasSettings)
@ -502,8 +491,8 @@ namespace osu.Game.Overlays.Mods
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
Ruleset.UnbindAll(); availableMods?.UnbindAll();
SelectedMods.UnbindAll(); SelectedMods?.UnbindAll();
} }
#endregion #endregion

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 osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
@ -13,9 +15,23 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage Icon => FontAwesome.Solid.Question; public override IconUsage Icon => FontAwesome.Solid.Question;
public override string Description => "Whoaaaaa..."; public override string Description => "Whoaaaaa...";
private readonly BindableNumber<double> tempoAdjust = new BindableDouble(1);
private readonly BindableNumber<double> freqAdjust = new BindableDouble(1);
protected ModDaycore()
{
SpeedChange.BindValueChanged(val =>
{
freqAdjust.Value = SpeedChange.Default;
tempoAdjust.Value = val.NewValue / SpeedChange.Default;
}, true);
}
public override void ApplyToTrack(Track track) public override void ApplyToTrack(Track track)
{ {
track.Frequency.Value *= RateAdjust; // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied)
track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust);
track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust);
} }
} }
} }

View File

@ -3,12 +3,14 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModDoubleTime : ModTimeAdjust public abstract class ModDoubleTime : ModRateAdjust
{ {
public override string Name => "Double Time"; public override string Name => "Double Time";
public override string Acronym => "DT"; public override string Acronym => "DT";
@ -19,6 +21,14 @@ namespace osu.Game.Rulesets.Mods
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModHalfTime)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModHalfTime)).ToArray();
protected override double RateAdjust => 1.5; [SettingSource("Speed increase", "The actual increase to apply")]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble
{
MinValue = 1.01,
MaxValue = 2,
Default = 1.5,
Value = 1.5,
Precision = 0.01,
};
} }
} }

View File

@ -3,12 +3,14 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModHalfTime : ModTimeAdjust public abstract class ModHalfTime : ModRateAdjust
{ {
public override string Name => "Half Time"; public override string Name => "Half Time";
public override string Acronym => "HT"; public override string Acronym => "HT";
@ -19,6 +21,14 @@ namespace osu.Game.Rulesets.Mods
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModDoubleTime)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModDoubleTime)).ToArray();
protected override double RateAdjust => 0.75; [SettingSource("Speed decrease", "The actual decrease to apply")]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble
{
MinValue = 0.5,
MaxValue = 0.99,
Default = 0.75,
Value = 0.75,
Precision = 0.01,
};
} }
} }

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 osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -14,9 +16,23 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage Icon => OsuIcon.ModNightcore; public override IconUsage Icon => OsuIcon.ModNightcore;
public override string Description => "Uguuuuuuuu..."; public override string Description => "Uguuuuuuuu...";
private readonly BindableNumber<double> tempoAdjust = new BindableDouble(1);
private readonly BindableNumber<double> freqAdjust = new BindableDouble(1);
protected ModNightcore()
{
SpeedChange.BindValueChanged(val =>
{
freqAdjust.Value = SpeedChange.Default;
tempoAdjust.Value = val.NewValue / SpeedChange.Default;
}, true);
}
public override void ApplyToTrack(Track track) public override void ApplyToTrack(Track track)
{ {
track.Frequency.Value *= RateAdjust; // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied)
track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust);
track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust);
} }
} }
} }

View File

@ -1,20 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModTimeAdjust : Mod, IApplicableToTrack public abstract class ModRateAdjust : Mod, IApplicableToTrack
{ {
public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp) }; public abstract BindableNumber<double> SpeedChange { get; }
protected abstract double RateAdjust { get; }
public virtual void ApplyToTrack(Track track) public virtual void ApplyToTrack(Track track)
{ {
track.Tempo.Value *= RateAdjust; track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange);
} }
} }
} }

View File

@ -3,42 +3,58 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToTrack, IApplicableToBeatmap public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToTrack
{ {
/// <summary> /// <summary>
/// The point in the beatmap at which the final ramping rate should be reached. /// The point in the beatmap at which the final ramping rate should be reached.
/// </summary> /// </summary>
private const double final_rate_progress = 0.75f; private const double final_rate_progress = 0.75f;
public override Type[] IncompatibleMods => new[] { typeof(ModTimeAdjust) }; [SettingSource("Final rate", "The final speed to ramp to")]
public abstract BindableNumber<double> FinalRate { get; }
protected abstract double FinalRateAdjustment { get; }
private double finalRateTime; private double finalRateTime;
private double beginRampTime; private double beginRampTime;
public BindableNumber<double> SpeedChange { get; } = new BindableDouble
{
Default = 1,
Value = 1,
Precision = 0.01,
};
private Track track; private Track track;
public virtual void ApplyToTrack(Track track) protected ModTimeRamp()
{
// for preview purpose at song select. eventually we'll want to be able to update every frame.
FinalRate.BindValueChanged(val => applyAdjustment(1), true);
}
public void ApplyToTrack(Track track)
{ {
this.track = track; this.track = track;
track.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
lastAdjust = 1; FinalRate.TriggerChange();
// for preview purposes. during gameplay, Update will overwrite this setting.
applyAdjustment(1);
} }
public virtual void ApplyToBeatmap(IBeatmap beatmap) public virtual void ApplyToBeatmap(IBeatmap beatmap)
{ {
HitObject lastObject = beatmap.HitObjects.LastOrDefault(); HitObject lastObject = beatmap.HitObjects.LastOrDefault();
SpeedChange.SetDefault();
beginRampTime = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0; beginRampTime = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0;
finalRateTime = final_rate_progress * (lastObject?.GetEndTime() ?? 0); finalRateTime = final_rate_progress * (lastObject?.GetEndTime() ?? 0);
} }
@ -48,20 +64,11 @@ namespace osu.Game.Rulesets.Mods
applyAdjustment((track.CurrentTime - beginRampTime) / finalRateTime); applyAdjustment((track.CurrentTime - beginRampTime) / finalRateTime);
} }
private double lastAdjust = 1;
/// <summary> /// <summary>
/// Adjust the rate along the specified ramp /// Adjust the rate along the specified ramp
/// </summary> /// </summary>
/// <param name="amount">The amount of adjustment to apply (from 0..1).</param> /// <param name="amount">The amount of adjustment to apply (from 0..1).</param>
private void applyAdjustment(double amount) private void applyAdjustment(double amount) =>
{ SpeedChange.Value = 1 + (FinalRate.Value - 1) * Math.Clamp(amount, 0, 1);
double adjust = 1 + (Math.Sign(FinalRateAdjustment) * Math.Clamp(amount, 0, 1) * Math.Abs(FinalRateAdjustment));
track.Tempo.Value /= lastAdjust;
track.Tempo.Value *= adjust;
lastAdjust = adjust;
}
} }
} }

View File

@ -3,7 +3,9 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -15,7 +17,15 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage Icon => FontAwesome.Solid.ChevronCircleDown; public override IconUsage Icon => FontAwesome.Solid.ChevronCircleDown;
public override double ScoreMultiplier => 1.0; public override double ScoreMultiplier => 1.0;
protected override double FinalRateAdjustment => -0.25; [SettingSource("Final rate", "The speed increase to ramp towards")]
public override BindableNumber<double> FinalRate { get; } = new BindableDouble
{
MinValue = 0.5,
MaxValue = 0.99,
Default = 0.75,
Value = 0.75,
Precision = 0.01,
};
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindUp)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindUp)).ToArray();
} }

View File

@ -3,7 +3,9 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -15,7 +17,15 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage Icon => FontAwesome.Solid.ChevronCircleUp; public override IconUsage Icon => FontAwesome.Solid.ChevronCircleUp;
public override double ScoreMultiplier => 1.0; public override double ScoreMultiplier => 1.0;
protected override double FinalRateAdjustment => 0.5; [SettingSource("Final rate", "The speed increase to ramp towards")]
public override BindableNumber<double> FinalRate { get; } = new BindableDouble
{
MinValue = 1.01,
MaxValue = 2,
Default = 1.5,
Value = 1.5,
Precision = 0.01,
};
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindDown)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindDown)).ToArray();
} }

View File

@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Objects
ApplyDefaultsToSelf(controlPointInfo, difficulty); ApplyDefaultsToSelf(controlPointInfo, difficulty);
// This is done here since ApplyDefaultsToSelf may be used to determine the end time // This is done here since ApplyDefaultsToSelf may be used to determine the end time
SampleControlPoint = controlPointInfo.SamplePointAt(((this as IHasEndTime)?.EndTime ?? StartTime) + control_point_leniency); SampleControlPoint = controlPointInfo.SamplePointAt(this.GetEndTime() + control_point_leniency);
nestedHitObjects.Clear(); nestedHitObjects.Clear();

View File

@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
result = CreateSlider(pos, combo, comboOffset, convertControlPoints(points, pathType), length, repeatCount, nodeSamples); result = CreateSlider(pos, combo, comboOffset, convertControlPoints(points, pathType), length, repeatCount, nodeSamples);
// The samples are played when the slider ends, which is the last node // The samples are played when the slider ends, which is the last node
result.Samples = nodeSamples[nodeSamples.Count - 1]; result.Samples = nodeSamples[^1];
} }
else if (type.HasFlag(ConvertHitObjectType.Spinner)) else if (type.HasFlag(ConvertHitObjectType.Spinner))
{ {
@ -279,7 +279,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
{ {
if (vertices[i] == vertices[i - 1]) if (vertices[i] == vertices[i - 1])
{ {
points[points.Count - 1].Type.Value = type; points[^1].Type.Value = type;
continue; continue;
} }

View File

@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Objects
get get
{ {
ensureValid(); ensureValid();
return cumulativeLength.Count == 0 ? 0 : cumulativeLength[cumulativeLength.Count - 1]; return cumulativeLength.Count == 0 ? 0 : cumulativeLength[^1];
} }
} }
@ -251,7 +251,7 @@ namespace osu.Game.Rulesets.Objects
if (calculatedLength > expectedDistance) if (calculatedLength > expectedDistance)
{ {
// The path will be shortened further, in which case we should trim any more unnecessary lengths and their associated path segments // The path will be shortened further, in which case we should trim any more unnecessary lengths and their associated path segments
while (cumulativeLength.Count > 0 && cumulativeLength[cumulativeLength.Count - 1] >= expectedDistance) while (cumulativeLength.Count > 0 && cumulativeLength[^1] >= expectedDistance)
{ {
cumulativeLength.RemoveAt(cumulativeLength.Count - 1); cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
calculatedPath.RemoveAt(pathEndIndex--); calculatedPath.RemoveAt(pathEndIndex--);
@ -269,7 +269,7 @@ namespace osu.Game.Rulesets.Objects
// The direction of the segment to shorten or lengthen // The direction of the segment to shorten or lengthen
Vector2 dir = (calculatedPath[pathEndIndex] - calculatedPath[pathEndIndex - 1]).Normalized(); Vector2 dir = (calculatedPath[pathEndIndex] - calculatedPath[pathEndIndex - 1]).Normalized();
calculatedPath[pathEndIndex] = calculatedPath[pathEndIndex - 1] + dir * (float)(expectedDistance - cumulativeLength[cumulativeLength.Count - 1]); calculatedPath[pathEndIndex] = calculatedPath[pathEndIndex - 1] + dir * (float)(expectedDistance - cumulativeLength[^1]);
cumulativeLength.Add(expectedDistance); cumulativeLength.Add(expectedDistance);
} }
} }

View File

@ -87,12 +87,12 @@ namespace osu.Game.Screens
public virtual float BackgroundParallaxAmount => 1; public virtual float BackgroundParallaxAmount => 1;
public virtual bool AllowRateAdjustments => true;
public Bindable<WorkingBeatmap> Beatmap { get; private set; } public Bindable<WorkingBeatmap> Beatmap { get; private set; }
public Bindable<RulesetInfo> Ruleset { get; private set; } public Bindable<RulesetInfo> Ruleset { get; private set; }
public virtual bool AllowRateAdjustments => true;
public Bindable<IReadOnlyList<Mod>> Mods { get; private set; } public Bindable<IReadOnlyList<Mod>> Mods { get; private set; }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)

View File

@ -26,16 +26,26 @@ namespace osu.Game.Screens
Beatmap = parent.Get<LeasedBindable<WorkingBeatmap>>()?.GetBoundCopy(); Beatmap = parent.Get<LeasedBindable<WorkingBeatmap>>()?.GetBoundCopy();
if (Beatmap == null) if (Beatmap == null)
{
Cache(Beatmap = parent.Get<Bindable<WorkingBeatmap>>().BeginLease(false)); Cache(Beatmap = parent.Get<Bindable<WorkingBeatmap>>().BeginLease(false));
CacheAs(Beatmap);
}
Ruleset = parent.Get<LeasedBindable<RulesetInfo>>()?.GetBoundCopy(); Ruleset = parent.Get<LeasedBindable<RulesetInfo>>()?.GetBoundCopy();
if (Ruleset == null) if (Ruleset == null)
{
Cache(Ruleset = parent.Get<Bindable<RulesetInfo>>().BeginLease(true)); Cache(Ruleset = parent.Get<Bindable<RulesetInfo>>().BeginLease(true));
CacheAs(Ruleset);
}
Mods = parent.Get<LeasedBindable<IReadOnlyList<Mod>>>()?.GetBoundCopy(); Mods = parent.Get<LeasedBindable<IReadOnlyList<Mod>>>()?.GetBoundCopy();
if (Mods == null) if (Mods == null)
{
Cache(Mods = parent.Get<Bindable<IReadOnlyList<Mod>>>().BeginLease(true)); Cache(Mods = parent.Get<Bindable<IReadOnlyList<Mod>>>().BeginLease(true));
CacheAs(Mods);
}
} }
else else
{ {

View File

@ -43,7 +43,7 @@ namespace osu.Game.Screens.Play
private readonly double firstHitObjectTime; private readonly double firstHitObjectTime;
public readonly Bindable<double> UserPlaybackRate = new BindableDouble(1) public readonly BindableNumber<double> UserPlaybackRate = new BindableDouble(1)
{ {
Default = 1, Default = 1,
MinValue = 0.5, MinValue = 0.5,
@ -73,7 +73,6 @@ namespace osu.Game.Screens.Play
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
track = beatmap.Track; track = beatmap.Track;
track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
@ -120,7 +119,6 @@ namespace osu.Game.Screens.Play
Seek(startTime); Seek(startTime);
adjustableClock.ProcessFrame(); adjustableClock.ProcessFrame();
UserPlaybackRate.ValueChanged += _ => updateRate();
} }
public void Restart() public void Restart()
@ -223,7 +221,8 @@ namespace osu.Game.Screens.Play
speedAdjustmentsApplied = true; speedAdjustmentsApplied = true;
track.ResetSpeedAdjustments(); track.ResetSpeedAdjustments();
track.Tempo.Value = UserPlaybackRate.Value; track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
foreach (var mod in mods.OfType<IApplicableToTrack>()) foreach (var mod in mods.OfType<IApplicableToTrack>())
mod.ApplyToTrack(track); mod.ApplyToTrack(track);
@ -244,8 +243,6 @@ namespace osu.Game.Screens.Play
track.ResetSpeedAdjustments(); track.ResetSpeedAdjustments();
speedAdjustmentsApplied = false; speedAdjustmentsApplied = false;
} }
track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
} }
} }
} }

View File

@ -31,7 +31,8 @@ namespace osu.Game.Screens.Play.HUD
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
processor.NewJudgement += onNewJudgement; if (processor != null)
processor.NewJudgement += onNewJudgement;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -96,7 +97,9 @@ namespace osu.Game.Screens.Play.HUD
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
processor.NewJudgement -= onNewJudgement;
if (processor != null)
processor.NewJudgement -= onNewJudgement;
} }
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -23,8 +24,8 @@ namespace osu.Game.Screens.Play
{ {
public class HUDOverlay : Container public class HUDOverlay : Container
{ {
private const int duration = 250; private const float fade_duration = 400;
private const Easing easing = Easing.OutQuint; private const Easing fade_easing = Easing.Out;
public readonly KeyCounterDisplay KeyCounter; public readonly KeyCounterDisplay KeyCounter;
public readonly RollingCounter<int> ComboCounter; public readonly RollingCounter<int> ComboCounter;
@ -43,8 +44,15 @@ namespace osu.Game.Screens.Play
private readonly DrawableRuleset drawableRuleset; private readonly DrawableRuleset drawableRuleset;
private readonly IReadOnlyList<Mod> mods; private readonly IReadOnlyList<Mod> mods;
private Bindable<bool> showHud; /// <summary>
/// Whether the elements that can optionally be hidden should be visible.
/// </summary>
public Bindable<bool> ShowHud { get; } = new BindableBool();
private Bindable<bool> configShowHud;
private readonly Container visibilityContainer; private readonly Container visibilityContainer;
private readonly BindableBool replayLoaded = new BindableBool(); private readonly BindableBool replayLoaded = new BindableBool();
private static bool hasShownNotificationOnce; private static bool hasShownNotificationOnce;
@ -53,6 +61,8 @@ namespace osu.Game.Screens.Play
private readonly Container topScoreContainer; private readonly Container topScoreContainer;
private IEnumerable<Drawable> hideTargets => new Drawable[] { visibilityContainer, KeyCounter };
public HUDOverlay(ScoreProcessor scoreProcessor, DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods) public HUDOverlay(ScoreProcessor scoreProcessor, DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods)
{ {
this.scoreProcessor = scoreProcessor; this.scoreProcessor = scoreProcessor;
@ -68,13 +78,12 @@ namespace osu.Game.Screens.Play
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
HealthDisplay = CreateHealthDisplay(),
topScoreContainer = new Container topScoreContainer = new Container
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
AutoSizeDuration = 200,
AutoSizeEasing = Easing.Out,
Children = new Drawable[] Children = new Drawable[]
{ {
AccuracyCounter = CreateAccuracyCounter(), AccuracyCounter = CreateAccuracyCounter(),
@ -82,19 +91,20 @@ namespace osu.Game.Screens.Play
ComboCounter = CreateComboCounter(), ComboCounter = CreateComboCounter(),
}, },
}, },
HealthDisplay = CreateHealthDisplay(),
Progress = CreateProgress(), Progress = CreateProgress(),
ModDisplay = CreateModsContainer(), ModDisplay = CreateModsContainer(),
HitErrorDisplay = CreateHitErrorDisplayOverlay(), HitErrorDisplay = CreateHitErrorDisplayOverlay(),
PlayerSettingsOverlay = CreatePlayerSettingsOverlay(),
} }
}, },
PlayerSettingsOverlay = CreatePlayerSettingsOverlay(),
new FillFlowContainer new FillFlowContainer
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Position = -new Vector2(5, TwoLayerButton.SIZE_RETRACTED.Y), Position = -new Vector2(5, TwoLayerButton.SIZE_RETRACTED.Y),
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
LayoutDuration = fade_duration / 2,
LayoutEasing = fade_easing,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
@ -108,34 +118,24 @@ namespace osu.Game.Screens.Play
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuConfigManager config, NotificationOverlay notificationOverlay) private void load(OsuConfigManager config, NotificationOverlay notificationOverlay)
{ {
BindProcessor(scoreProcessor); if (scoreProcessor != null)
BindDrawableRuleset(drawableRuleset); BindProcessor(scoreProcessor);
Progress.Objects = drawableRuleset.Objects; if (drawableRuleset != null)
Progress.AllowSeeking = drawableRuleset.HasReplayLoaded.Value; {
Progress.RequestSeek = time => RequestSeek(time); BindDrawableRuleset(drawableRuleset);
Progress.ReferenceClock = drawableRuleset.FrameStableClock;
Progress.Objects = drawableRuleset.Objects;
Progress.AllowSeeking = drawableRuleset.HasReplayLoaded.Value;
Progress.RequestSeek = time => RequestSeek(time);
Progress.ReferenceClock = drawableRuleset.FrameStableClock;
}
ModDisplay.Current.Value = mods; ModDisplay.Current.Value = mods;
showHud = config.GetBindable<bool>(OsuSetting.ShowInterface); configShowHud = config.GetBindable<bool>(OsuSetting.ShowInterface);
showHud.BindValueChanged(visible => visibilityContainer.FadeTo(visible.NewValue ? 1 : 0, duration, easing), true);
ShowHealthbar.BindValueChanged(healthBar => if (!configShowHud.Value && !hasShownNotificationOnce)
{
if (healthBar.NewValue)
{
HealthDisplay.FadeIn(duration, easing);
topScoreContainer.MoveToY(30, duration, easing);
}
else
{
HealthDisplay.FadeOut(duration, easing);
topScoreContainer.MoveToY(0, duration, easing);
}
}, true);
if (!showHud.Value && !hasShownNotificationOnce)
{ {
hasShownNotificationOnce = true; hasShownNotificationOnce = true;
@ -144,12 +144,39 @@ namespace osu.Game.Screens.Play
Text = @"The score overlay is currently disabled. You can toggle this by pressing Shift+Tab." Text = @"The score overlay is currently disabled. You can toggle this by pressing Shift+Tab."
}); });
} }
// start all elements hidden
hideTargets.ForEach(d => d.Hide());
} }
public override void Hide() => throw new InvalidOperationException($"{nameof(HUDOverlay)} should not be hidden as it will remove the ability of a user to quit. Use {nameof(ShowHud)} instead.");
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
ShowHud.BindValueChanged(visible => hideTargets.ForEach(d => d.FadeTo(visible.NewValue ? 1 : 0, fade_duration, fade_easing)));
ShowHealthbar.BindValueChanged(healthBar =>
{
if (healthBar.NewValue)
{
HealthDisplay.FadeIn(fade_duration, fade_easing);
topScoreContainer.MoveToY(30, fade_duration, fade_easing);
}
else
{
HealthDisplay.FadeOut(fade_duration, fade_easing);
topScoreContainer.MoveToY(0, fade_duration, fade_easing);
}
}, true);
configShowHud.BindValueChanged(visible =>
{
if (!ShowHud.Disabled)
ShowHud.Value = visible.NewValue;
}, true);
replayLoaded.BindValueChanged(replayLoadedValueChanged, true); replayLoaded.BindValueChanged(replayLoadedValueChanged, true);
} }
@ -189,7 +216,7 @@ namespace osu.Game.Screens.Play
switch (e.Key) switch (e.Key)
{ {
case Key.Tab: case Key.Tab:
showHud.Value = !showHud.Value; configShowHud.Value = !configShowHud.Value;
return true; return true;
} }
} }
@ -257,7 +284,7 @@ namespace osu.Game.Screens.Play
Margin = new MarginPadding { Top = 20, Right = 10 }, Margin = new MarginPadding { Top = 20, Right = 10 },
}; };
protected virtual HitErrorDisplay CreateHitErrorDisplayOverlay() => new HitErrorDisplay(scoreProcessor, drawableRuleset.FirstAvailableHitWindows); protected virtual HitErrorDisplay CreateHitErrorDisplayOverlay() => new HitErrorDisplay(scoreProcessor, drawableRuleset?.FirstAvailableHitWindows);
protected virtual PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay(); protected virtual PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay();

View File

@ -587,13 +587,16 @@ namespace osu.Game.Screens.Select
switch (d) switch (d)
{ {
case DrawableCarouselBeatmapSet set: case DrawableCarouselBeatmapSet set:
{
lastSet = set; lastSet = set;
set.MoveToX(set.Item.State.Value == CarouselItemState.Selected ? -100 : 0, 500, Easing.OutExpo); set.MoveToX(set.Item.State.Value == CarouselItemState.Selected ? -100 : 0, 500, Easing.OutExpo);
set.MoveToY(currentY, 750, Easing.OutExpo); set.MoveToY(currentY, 750, Easing.OutExpo);
break; break;
}
case DrawableCarouselBeatmap beatmap: case DrawableCarouselBeatmap beatmap:
{
if (beatmap.Item.State.Value == CarouselItemState.Selected) if (beatmap.Item.State.Value == CarouselItemState.Selected)
scrollTarget = currentY + beatmap.DrawHeight / 2 - DrawHeight / 2; scrollTarget = currentY + beatmap.DrawHeight / 2 - DrawHeight / 2;
@ -619,6 +622,7 @@ namespace osu.Game.Screens.Select
} }
break; break;
}
} }
} }

View File

@ -3,77 +3,39 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Database; using osu.Game.Database;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
public class LegacySkinResourceStore<T> : IResourceStore<byte[]> public class LegacySkinResourceStore<T> : ResourceStore<byte[]>
where T : INamedFileInfo where T : INamedFileInfo
{ {
private readonly IHasFiles<T> source; private readonly IHasFiles<T> source;
private readonly IResourceStore<byte[]> underlyingStore;
private string getPathForFile(string filename)
{
if (source.Files == null)
return null;
bool hasExtension = filename.Contains('.');
var file = source.Files.Find(f =>
string.Equals(hasExtension ? f.Filename : Path.ChangeExtension(f.Filename, null), filename, StringComparison.InvariantCultureIgnoreCase));
return file?.FileInfo.StoragePath;
}
public LegacySkinResourceStore(IHasFiles<T> source, IResourceStore<byte[]> underlyingStore) public LegacySkinResourceStore(IHasFiles<T> source, IResourceStore<byte[]> underlyingStore)
: base(underlyingStore)
{ {
this.source = source; this.source = source;
this.underlyingStore = underlyingStore;
} }
public Stream GetStream(string name) protected override IEnumerable<string> GetFilenames(string name)
{ {
string path = getPathForFile(name); if (source.Files == null)
return path == null ? null : underlyingStore.GetStream(path); yield break;
}
public IEnumerable<string> GetAvailableResources() => source.Files.Select(f => f.Filename); foreach (var filename in base.GetFilenames(name))
byte[] IResourceStore<byte[]>.Get(string name) => GetAsync(name).Result;
public Task<byte[]> GetAsync(string name)
{
string path = getPathForFile(name);
return path == null ? Task.FromResult<byte[]>(null) : underlyingStore.GetAsync(path);
}
#region IDisposable Support
private bool isDisposed;
protected virtual void Dispose(bool disposing)
{
if (!isDisposed)
{ {
isDisposed = true; var path = getPathForFile(filename);
if (path != null)
yield return path;
} }
} }
~LegacySkinResourceStore() private string getPathForFile(string filename) =>
{ source.Files.Find(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase))?.FileInfo.StoragePath;
Dispose(false);
}
public void Dispose() public override IEnumerable<string> GetAvailableResources() => source.Files.Select(f => f.Filename);
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
} }
} }

View File

@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual
var working = CreateWorkingBeatmap(rulesetInfo); var working = CreateWorkingBeatmap(rulesetInfo);
Beatmap.Value = working; Beatmap.Value = working;
Mods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) }; SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) };
Player?.Exit(); Player?.Exit();
Player = null; Player = null;

View File

@ -12,7 +12,6 @@ using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -21,6 +20,7 @@ using osu.Game.Database;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
@ -28,21 +28,13 @@ namespace osu.Game.Tests.Visual
{ {
public abstract class OsuTestScene : TestScene public abstract class OsuTestScene : TestScene
{ {
[Cached(typeof(Bindable<WorkingBeatmap>))] protected Bindable<WorkingBeatmap> Beatmap { get; private set; }
[Cached(typeof(IBindable<WorkingBeatmap>))]
private NonNullableBindable<WorkingBeatmap> beatmap;
protected Bindable<WorkingBeatmap> Beatmap => beatmap; protected Bindable<RulesetInfo> Ruleset;
[Cached] protected Bindable<IReadOnlyList<Mod>> SelectedMods;
[Cached(typeof(IBindable<RulesetInfo>))]
protected readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
[Cached] protected new OsuScreenDependencies Dependencies { get; private set; }
[Cached(Type = typeof(IBindable<IReadOnlyList<Mod>>))]
protected readonly Bindable<IReadOnlyList<Mod>> Mods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
protected new DependencyContainer Dependencies { get; private set; }
private readonly Lazy<Storage> localStorage; private readonly Lazy<Storage> localStorage;
protected Storage LocalStorage => localStorage.Value; protected Storage LocalStorage => localStorage.Value;
@ -72,18 +64,16 @@ namespace osu.Game.Tests.Visual
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{ {
// This is the earliest we can get OsuGameBase, which is used by the dummy working beatmap to find textures Dependencies = new OsuScreenDependencies(false, base.CreateChildDependencies(parent));
var working = new DummyWorkingBeatmap(parent.Get<AudioManager>(), parent.Get<TextureStore>());
beatmap = new NonNullableBindable<WorkingBeatmap>(working) { Default = working }; Beatmap = Dependencies.Beatmap;
beatmap.BindValueChanged(b => ScheduleAfterChildren(() => Beatmap.SetDefault();
{
// compare to last beatmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo)
if (b.OldValue?.TrackLoaded == true && b.OldValue?.Track != b.NewValue?.Track)
b.OldValue.RecycleTrack();
}));
Dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); Ruleset = Dependencies.Ruleset;
Ruleset.SetDefault();
SelectedMods = Dependencies.Mods;
SelectedMods.SetDefault();
if (!UseOnlineAPI) if (!UseOnlineAPI)
{ {
@ -135,8 +125,8 @@ namespace osu.Game.Tests.Visual
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
if (beatmap?.Value.TrackLoaded == true) if (Beatmap?.Value.TrackLoaded == true)
beatmap.Value.Track.Stop(); Beatmap.Value.Track.Stop();
if (contextFactory.IsValueCreated) if (contextFactory.IsValueCreated)
contextFactory.Value.ResetDatabase(); contextFactory.Value.ResetDatabase();

View File

@ -53,14 +53,14 @@ namespace osu.Game.Tests.Visual
{ {
var noFailMod = ruleset.GetAllMods().FirstOrDefault(m => m is ModNoFail); var noFailMod = ruleset.GetAllMods().FirstOrDefault(m => m is ModNoFail);
if (noFailMod != null) if (noFailMod != null)
Mods.Value = new[] { noFailMod }; SelectedMods.Value = new[] { noFailMod };
} }
if (Autoplay) if (Autoplay)
{ {
var mod = ruleset.GetAutoplayMod(); var mod = ruleset.GetAutoplayMod();
if (mod != null) if (mod != null)
Mods.Value = Mods.Value.Concat(mod.Yield()).ToArray(); SelectedMods.Value = SelectedMods.Value.Concat(mod.Yield()).ToArray();
} }
Player = CreatePlayer(ruleset); Player = CreatePlayer(ruleset);

View File

@ -22,8 +22,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.1215.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.1212.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.1215.0" />
<PackageReference Include="Sentry" Version="1.2.0" /> <PackageReference Include="Sentry" Version="1.2.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" /> <PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />

View File

@ -73,8 +73,8 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.1215.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.1212.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.1215.0" />
</ItemGroup> </ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. --> <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
@ -82,7 +82,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2019.1212.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.1215.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" /> <PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />

View File

@ -215,11 +215,12 @@
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedMethodReturnValue_002ELocal/@EntryIndexedValue">HINT</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedMethodReturnValue_002ELocal/@EntryIndexedValue">HINT</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedParameter_002EGlobal/@EntryIndexedValue">HINT</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedParameter_002EGlobal/@EntryIndexedValue">HINT</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedParameter_002ELocal/@EntryIndexedValue">HINT</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedParameter_002ELocal/@EntryIndexedValue">HINT</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedType_002EGlobal/@EntryIndexedValue">HINT</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseAwaitUsing/@EntryIndexedValue">DO_NOT_SHOW</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseAwaitUsing/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseCollectionCountProperty/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseCollectionCountProperty/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseFormatSpecifierInFormatString/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseFormatSpecifierInFormatString/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseFormatSpecifierInInterpolation/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseFormatSpecifierInInterpolation/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseIndexFromEndExpression/@EntryIndexedValue">DO_NOT_SHOW</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseIndexFromEndExpression/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNameofExpression/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNameofExpression/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNegatedPatternMatching/@EntryIndexedValue"></s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNegatedPatternMatching/@EntryIndexedValue"></s:String>
<s:Boolean x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNegatedPatternMatching/@EntryIndexRemoved">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNegatedPatternMatching/@EntryIndexRemoved">True</s:Boolean>