diff --git a/osu.Android.props b/osu.Android.props
index f271bdfaaf..0ebb6be7a1 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -10,7 +10,7 @@
true
-
+
Release Difference / ms
// release_threshold
if (isOverlapping)
- holdAddition = 1 / (1 + Math.Exp(0.27 * (release_threshold - closestEndTime)));
+ holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold);
// Decay and increase individualStrains in own column
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs
index 6a12ec5088..5cfcf00b33 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs
@@ -3,21 +3,39 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
-using osu.Game.Rulesets.Mania.Skinning.Default;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
{
- public partial class EditBodyPiece : DefaultBodyPiece
+ public partial class EditBodyPiece : CompositeDrawable
{
+ private readonly Container border;
+
+ public EditBodyPiece()
+ {
+ InternalChildren = new Drawable[]
+ {
+ border = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ BorderThickness = 3,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ },
+ },
+ };
+ }
+
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- AccentColour.Value = colours.Yellow;
-
- Background.Alpha = 0.5f;
+ border.BorderColour = colours.YellowDarker;
}
-
- protected override Drawable CreateForeground() => base.CreateForeground().With(d => d.Alpha = 0);
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditHoldNoteEndPiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditHoldNoteEndPiece.cs
index 0aa72c28b8..d4b61b4661 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditHoldNoteEndPiece.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditHoldNoteEndPiece.cs
@@ -4,6 +4,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
@@ -26,10 +27,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
{
Height = DefaultNotePiece.NOTE_HEIGHT;
- CornerRadius = 5;
- Masking = true;
-
- InternalChild = new DefaultNotePiece();
+ InternalChild = new EditNotePiece
+ {
+ RelativeSizeAxes = Axes.Both,
+ Height = 1,
+ };
}
protected override void LoadComplete()
@@ -60,19 +62,23 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
{
base.OnDrag(e);
Dragging?.Invoke(e.ScreenSpaceMousePosition);
+ updateState();
}
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
DragEnded?.Invoke();
+ updateState();
}
private void updateState()
{
+ InternalChild.Colour = Colour4.White;
+
var colour = colours.Yellow;
- if (IsHovered)
+ if (IsHovered || IsDragged)
colour = colour.Lighten(1);
Colour = colour;
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs
index 48dde29a9f..f68004db28 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs
@@ -2,28 +2,63 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
-using osu.Game.Rulesets.Mania.Skinning.Default;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
{
public partial class EditNotePiece : CompositeDrawable
{
+ private readonly Container border;
+ private readonly Box box;
+
+ [Resolved]
+ private Column? column { get; set; }
+
public EditNotePiece()
{
- Height = DefaultNotePiece.NOTE_HEIGHT;
-
- CornerRadius = 5;
- Masking = true;
-
- InternalChild = new DefaultNotePiece();
+ InternalChildren = new Drawable[]
+ {
+ border = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ BorderThickness = 3,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ },
+ },
+ box = new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 3,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ },
+ };
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- Colour = colours.Yellow;
+ border.BorderColour = colours.YellowDark;
+ box.Colour = colours.YellowLight;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (column != null)
+ Scale = new Vector2(1, column.ScrollingInfo.Direction.Value == ScrollingDirection.Down ? 1 : -1);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
index 991b7f476c..13cfc5f691 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
@@ -4,8 +4,10 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
+using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
@@ -17,9 +19,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public partial class HoldNotePlacementBlueprint : ManiaPlacementBlueprint
{
- private readonly EditBodyPiece bodyPiece;
- private readonly EditNotePiece headPiece;
- private readonly EditNotePiece tailPiece;
+ private EditBodyPiece bodyPiece = null!;
+ private Circle headPiece = null!;
+ private Circle tailPiece = null!;
[Resolved]
private IScrollingInfo scrollingInfo { get; set; } = null!;
@@ -28,14 +30,29 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
public HoldNotePlacementBlueprint()
: base(new HoldNote())
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
bodyPiece = new EditBodyPiece { Origin = Anchor.TopCentre },
- headPiece = new EditNotePiece { Origin = Anchor.Centre },
- tailPiece = new EditNotePiece { Origin = Anchor.Centre }
+ headPiece = new Circle
+ {
+ Origin = Anchor.Centre,
+ Colour = colours.Yellow,
+ Height = 10
+ },
+ tailPiece = new Circle
+ {
+ Origin = Anchor.Centre,
+ Colour = colours.Yellow,
+ Height = 10
+ },
};
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index b8e6aa26a0..915706c044 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -2,14 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osuTK;
@@ -17,9 +17,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public partial class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint
{
- [Resolved]
- private OsuColour colours { get; set; } = null!;
-
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
@@ -29,9 +26,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[Resolved]
private IPositionSnapProvider? positionSnapProvider { get; set; }
+ private EditBodyPiece body = null!;
private EditHoldNoteEndPiece head = null!;
private EditHoldNoteEndPiece tail = null!;
+ protected new DrawableHoldNote DrawableObject => (DrawableHoldNote)base.DrawableObject;
+
public HoldNoteSelectionBlueprint(HoldNote hold)
: base(hold)
{
@@ -42,9 +42,17 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
InternalChildren = new Drawable[]
{
+ body = new EditBodyPiece
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ },
head = new EditHoldNoteEndPiece
{
RelativeSizeAxes = Axes.X,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
DragStarted = () => changeHandler?.BeginChange(),
Dragging = pos =>
{
@@ -64,6 +72,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
tail = new EditHoldNoteEndPiece
{
RelativeSizeAxes = Axes.X,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
DragStarted = () => changeHandler?.BeginChange(),
Dragging = pos =>
{
@@ -79,19 +89,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
},
DragEnded = () => changeHandler?.EndChange(),
},
- new Container
- {
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- BorderThickness = 1,
- BorderColour = colours.Yellow,
- Child = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- AlwaysPresent = true,
- }
- }
};
}
@@ -99,11 +96,23 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.Update();
+ head.Height = DrawableObject.Head.DrawHeight;
head.Y = HitObjectContainer.PositionAtTime(HitObject.Head.StartTime, HitObject.StartTime);
+ tail.Height = DrawableObject.Tail.DrawHeight;
tail.Y = HitObjectContainer.PositionAtTime(HitObject.Tail.StartTime, HitObject.StartTime);
Height = HitObjectContainer.LengthAtTime(HitObject.StartTime, HitObject.EndTime) + tail.DrawHeight;
}
+ protected override void OnDirectionChanged(ValueChangedEvent direction)
+ {
+ Origin = direction.NewValue == ScrollingDirection.Down ? Anchor.BottomCentre : Anchor.TopCentre;
+
+ foreach (var child in InternalChildren)
+ child.Anchor = Origin;
+
+ head.Scale = tail.Scale = body.Scale = new Vector2(1, direction.NewValue == ScrollingDirection.Down ? 1 : -1);
+ }
+
public override Quad SelectionQuad => ScreenSpaceDrawQuad;
public override Vector2 ScreenSpaceSelectionPoint => head.ScreenSpaceDrawQuad.Centre;
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
index c645ddd98d..4bb9d5f5c1 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
@@ -37,16 +37,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
protected override void LoadComplete()
{
base.LoadComplete();
- directionBindable.BindValueChanged(onDirectionChanged, true);
+ directionBindable.BindValueChanged(OnDirectionChanged, true);
}
- private void onDirectionChanged(ValueChangedEvent direction)
- {
- var anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
- Anchor = Origin = anchor;
- foreach (var child in InternalChildren)
- child.Anchor = child.Origin = anchor;
- }
+ protected abstract void OnDirectionChanged(ValueChangedEvent direction);
protected override void Update()
{
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
index b3ec3ef3e4..422215db57 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
@@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osuTK.Input;
@@ -12,14 +14,25 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public partial class NotePlacementBlueprint : ManiaPlacementBlueprint
{
- private readonly EditNotePiece piece;
+ private Circle piece = null!;
public NotePlacementBlueprint()
: base(new Note())
{
- RelativeSizeAxes = Axes.Both;
+ }
- InternalChild = piece = new EditNotePiece { Origin = Anchor.Centre };
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ RelativeSizeAxes = Axes.Both;
+ Masking = true;
+
+ InternalChild = piece = new Circle
+ {
+ Origin = Anchor.Centre,
+ Colour = colours.Yellow,
+ Height = 10
+ };
}
public override void UpdateTimeAndPosition(SnapResult result)
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
index 01c7bd502a..3476f91568 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
@@ -1,18 +1,42 @@
// Copyright (c) ppy Pty Ltd . 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.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public partial class NoteSelectionBlueprint : ManiaSelectionBlueprint
{
+ private readonly EditNotePiece notePiece;
+
public NoteSelectionBlueprint(Note note)
: base(note)
{
- AddInternal(new EditNotePiece { RelativeSizeAxes = Axes.X });
+ Anchor = Anchor.BottomCentre;
+ Origin = Anchor.BottomCentre;
+ AddInternal(notePiece = new EditNotePiece
+ {
+ RelativeSizeAxes = Axes.X,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ });
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ notePiece.Height = DrawableObject.DrawHeight;
+ }
+
+ protected override void OnDirectionChanged(ValueChangedEvent direction)
+ {
+ notePiece.Scale = new Vector2(1, direction.NewValue == ScrollingDirection.Down ? 1 : -1);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
index e3b4fa2fb7..926a4b2736 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Mania.Edit
base.Update();
if (screenWithTimeline?.TimelineArea.Timeline != null)
- drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom / 2;
+ drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom.Value / 2;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs
index 758c8dd347..71618a4bc3 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs
@@ -54,7 +54,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
},
columnBackgrounds = new ColumnFlow(stageDefinition)
{
- RelativeSizeAxes = Axes.Y
+ RelativeSizeAxes = Axes.Y,
+ Masking = false,
},
new HitTargetInsetContainer
{
@@ -126,8 +127,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
},
new Container
{
+ X = isLastColumn ? -0.16f : 0,
Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Y,
Width = rightLineWidth,
Scale = new Vector2(0.740f, 1),
diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
index f444448797..5614a13a48 100644
--- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
+++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
@@ -28,6 +28,12 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly FillFlowContainer> columns;
private readonly StageDefinition stageDefinition;
+ public new bool Masking
+ {
+ get => base.Masking;
+ set => base.Masking = value;
+ }
+
public ColumnFlow(StageDefinition stageDefinition)
{
this.stageDefinition = stageDefinition;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index 3d1939acac..9816f6d0a4 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -3,6 +3,7 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
@@ -33,6 +34,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1);
+ const int radius = OsuDifficultyHitObject.NORMALISED_RADIUS;
+ const int diameter = OsuDifficultyHitObject.NORMALISED_DIAMETER;
+
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
@@ -77,14 +81,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
wideAngleBonus = calcWideAngleBonus(currAngle);
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
- if (osuCurrObj.StrainTime > 100) // Only buff deltaTime exceeding 300 bpm 1/2.
+ if (DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2) < 300) // Only buff deltaTime exceeding 300 bpm 1/2.
acuteAngleBonus = 0;
else
{
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
- * Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
+ * Math.Min(angleBonus, diameter * 1.25 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
- * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
+ * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, radius, diameter) - radius) / radius), 2); // Buff distance exceeding radius up to diameter.
}
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
@@ -104,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2);
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
- double overlapVelocityBuff = Math.Min(125 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity));
+ double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity));
velocityChangeBonus = overlapVelocityBuff * distRatio;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs
index d10d2c5c05..d503dd2bcc 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
@@ -120,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
islandCount.Count++;
// repeated island (ex: triplet -> triplet)
- double power = logistic(island.Delta, 2.75, 0.24, 14);
+ double power = DifficultyCalculationUtils.Logistic(island.Delta, maxValue: 2.75, multiplier: 0.24, midpointOffset: 58.33);
effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power));
islandCounts[countIndex] = (islandCount.Island, islandCount.Count);
@@ -172,8 +173,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though)
}
- private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - (multiplier * x))));
-
private class Island : IEquatable
{
private readonly double deltaDifferenceEpsilon;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs
index c220352ee0..a5f6468f17 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs
@@ -3,6 +3,7 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
@@ -10,8 +11,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{
public static class SpeedEvaluator
{
- private const double single_spacing_threshold = 125; // 1.25 circles distance between centers
- private const double min_speed_bonus = 75; // ~200BPM
+ private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
+ private const double min_speed_bonus = 200; // 200 BPM 1/4th
private const double speed_balancing_factor = 40;
private const double distance_multiplier = 0.94;
@@ -43,8 +44,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double speedBonus = 0.0;
// Add additional scaling bonus for streams/bursts higher than 200bpm
- if (strainTime < min_speed_bonus)
- speedBonus = 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
+ if (DifficultyCalculationUtils.MillisecondsToBPM(strainTime) > min_speed_bonus)
+ speedBonus = 0.75 * Math.Pow((DifficultyCalculationUtils.BPMToMilliseconds(min_speed_bonus) - strainTime) / speed_balancing_factor, 2);
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
double distance = travelDistance + osuCurrObj.MinimumJumpDistance;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 3eaf500ad7..5e4c5c1ee9 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
///
public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
+ public const int NORMALISED_DIAMETER = NORMALISED_RADIUS * 2;
+
public const int MIN_DELTA_TIME = 25;
private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
index 559a871df1..6823512cef 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
@@ -23,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
protected virtual double ReducedStrainBaseline => 0.75;
- protected double Difficulty;
-
protected OsuStrainSkill(Mod[] mods)
: base(mods)
{
@@ -32,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
public override double DifficultyValue()
{
- Difficulty = 0;
+ double difficulty = 0;
double weight = 1;
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
@@ -52,11 +50,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
// We're sorting from highest to lowest strain.
foreach (double strain in strains.OrderDescending())
{
- Difficulty += strain * weight;
+ difficulty += strain * weight;
weight *= DecayWeight;
}
- return Difficulty;
+ return difficulty;
}
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
diff --git a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs
index a2ee4a888d..695ff516b1 100644
--- a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs
+++ b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs
@@ -50,9 +50,14 @@ namespace osu.Game.Rulesets.Osu.Edit
[Resolved]
private HitObjectComposer composer { get; set; } = null!;
+ private Bindable newComboState = null!;
+
[BackgroundDependencyLoader]
private void load()
{
+ var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler;
+ newComboState = selectionHandler.SelectionNewComboState.GetBoundCopy();
+
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
Child = new FillFlowContainer
@@ -120,10 +125,11 @@ namespace osu.Game.Rulesets.Osu.Edit
changeHandler?.BeginChange();
began = true;
- distanceSnapInput.Current.BindValueChanged(_ => tryCreatePolygon());
- offsetAngleInput.Current.BindValueChanged(_ => tryCreatePolygon());
- repeatCountInput.Current.BindValueChanged(_ => tryCreatePolygon());
- pointInput.Current.BindValueChanged(_ => tryCreatePolygon());
+ distanceSnapInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
+ offsetAngleInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
+ repeatCountInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
+ pointInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
+ newComboState.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
tryCreatePolygon();
}
@@ -138,39 +144,69 @@ namespace osu.Game.Rulesets.Osu.Edit
double length = distanceSnapInput.Current.Value * velocity * timeSpacing;
float polygonRadius = (float)(length / (2 * Math.Sin(double.Pi / pointInput.Current.Value)));
- editorBeatmap.RemoveRange(insertedCircles);
- insertedCircles.Clear();
+ int totalPoints = pointInput.Current.Value * repeatCountInput.Current.Value;
- var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler;
- bool first = true;
-
- for (int i = 1; i <= pointInput.Current.Value * repeatCountInput.Current.Value; ++i)
+ if (insertedCircles.Count > totalPoints)
{
- float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + i * (2 * float.Pi / pointInput.Current.Value);
- var position = OsuPlayfield.BASE_SIZE / 2 + new Vector2(polygonRadius * float.Cos(angle), polygonRadius * float.Sin(angle));
+ editorBeatmap.RemoveRange(insertedCircles.GetRange(totalPoints, insertedCircles.Count - totalPoints));
+ insertedCircles.RemoveRange(totalPoints, insertedCircles.Count - totalPoints);
+ }
- var circle = new HitCircle
+ var newlyAdded = new List();
+
+ for (int i = 0; i < totalPoints; ++i)
+ {
+ float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + (i + 1) * (2 * float.Pi / pointInput.Current.Value);
+ var position = OsuPlayfield.BASE_SIZE / 2 + new Vector2(polygonRadius * float.Cos(angle), polygonRadius * float.Sin(angle));
+ bool newCombo = i == 0 && newComboState.Value == TernaryState.True;
+
+ HitCircle circle;
+
+ if (i < insertedCircles.Count)
{
- Position = position,
- StartTime = startTime,
- NewCombo = first && selectionHandler.SelectionNewComboState.Value == TernaryState.True,
- };
- // TODO: probably ensure samples also follow current ternary status (not trivial)
- circle.Samples.Add(circle.CreateHitSampleInfo());
+ circle = insertedCircles[i];
+
+ circle.Position = position;
+ circle.StartTime = startTime;
+ circle.NewCombo = newCombo;
+
+ editorBeatmap.Update(circle);
+ }
+ else
+ {
+ circle = new HitCircle
+ {
+ Position = position,
+ StartTime = startTime,
+ NewCombo = newCombo,
+ };
+
+ newlyAdded.Add(circle);
+
+ // TODO: probably ensure samples also follow current ternary status (not trivial)
+ circle.Samples.Add(circle.CreateHitSampleInfo());
+ }
if (position.X < 0 || position.Y < 0 || position.X > OsuPlayfield.BASE_SIZE.X || position.Y > OsuPlayfield.BASE_SIZE.Y)
{
commitButton.Enabled.Value = false;
+ editorBeatmap.RemoveRange(insertedCircles);
+ insertedCircles.Clear();
return;
}
- insertedCircles.Add(circle);
startTime = beatSnapProvider.SnapTime(startTime + timeSpacing);
-
- first = false;
}
- editorBeatmap.AddRange(insertedCircles);
+ var previousNewComboState = newComboState.Value;
+
+ insertedCircles.AddRange(newlyAdded);
+ editorBeatmap.AddRange(newlyAdded);
+
+ // When adding new hitObjects, newCombo state will get reset to false when no objects are selected.
+ // Since this is the case when this popover is showing, we need to restore the previous newCombo state
+ newComboState.Value = previousNewComboState;
+
commitButton.Enabled.Value = true;
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBloom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBloom.cs
new file mode 100644
index 0000000000..c674074dc6
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBloom.cs
@@ -0,0 +1,85 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Bindables;
+using osu.Framework.Localisation;
+using osu.Framework.Utils;
+using osu.Game.Configuration;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.UI.Cursor;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModBloom : Mod, IApplicableToScoreProcessor, IUpdatableByPlayfield, IApplicableToPlayer
+ {
+ public override string Name => "Bloom";
+ public override string Acronym => "BM";
+ public override ModType Type => ModType.Fun;
+ public override LocalisableString Description => "The cursor blooms into.. a larger cursor!";
+ public override double ScoreMultiplier => 1;
+ protected const float MIN_SIZE = 1;
+ protected const float TRANSITION_DURATION = 100;
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight), typeof(OsuModNoScope), typeof(ModTouchDevice) };
+
+ protected readonly BindableNumber CurrentCombo = new BindableInt();
+ protected readonly IBindable IsBreakTime = new Bindable();
+
+ private float currentSize;
+
+ [SettingSource(
+ "Max size at combo",
+ "The combo count at which the cursor reaches its maximum size",
+ SettingControlType = typeof(SettingsSlider>)
+ )]
+ public BindableInt MaxSizeComboCount { get; } = new BindableInt(50)
+ {
+ MinValue = 5,
+ MaxValue = 100,
+ };
+
+ [SettingSource(
+ "Final size multiplier",
+ "The multiplier applied to cursor size when combo reaches maximum",
+ SettingControlType = typeof(SettingsSlider>)
+ )]
+ public BindableFloat MaxCursorSize { get; } = new BindableFloat(10f)
+ {
+ MinValue = 5f,
+ MaxValue = 15f,
+ Precision = 0.5f,
+ };
+
+ public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
+
+ public void ApplyToPlayer(Player player)
+ {
+ IsBreakTime.BindTo(player.IsBreakTime);
+ }
+
+ public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
+ {
+ CurrentCombo.BindTo(scoreProcessor.Combo);
+ CurrentCombo.BindValueChanged(combo =>
+ {
+ currentSize = Math.Clamp(MaxCursorSize.Value * ((float)combo.NewValue / MaxSizeComboCount.Value), MIN_SIZE, MaxCursorSize.Value);
+ }, true);
+ }
+
+ public void Update(Playfield playfield)
+ {
+ OsuCursor cursor = (OsuCursor)(playfield.Cursor!.ActiveCursor);
+
+ if (IsBreakTime.Value)
+ cursor.ModScaleAdjust.Value = 1;
+ else
+ cursor.ModScaleAdjust.Value = (float)Interpolation.Lerp(cursor.ModScaleAdjust.Value, currentSize, Math.Clamp(cursor.Time.Elapsed / TRANSITION_DURATION, 0, 1));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
index 5a6cc50082..3009530b50 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public partial class OsuModFlashlight : ModFlashlight, IApplicableToDrawableHitObject
{
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
- public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModBlinds)).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModBloom), typeof(OsuModBlinds) }).ToArray();
private const double default_follow_delay = 120;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
index d1bbae8e1a..57d540a7d4 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
@@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override LocalisableString Description => "Where's the cursor?";
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModBloom) };
+
private PeriodTracker spinnerPeriods = null!;
public override BindableInt HiddenComboCount { get; } = new BindableInt(10)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
index 917685cdad..a364190a00 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
@@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModTouchDevice : ModTouchDevice
{
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModBloom) }).ToArray();
public override bool Ranked => UsesDefaultConfiguration;
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 2f928aaefa..25b1dd9b12 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -214,7 +214,8 @@ namespace osu.Game.Rulesets.Osu
new OsuModFreezeFrame(),
new OsuModBubbles(),
new OsuModSynesthesia(),
- new OsuModDepth()
+ new OsuModDepth(),
+ new OsuModBloom()
};
case ModType.System:
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index a4bccb0aff..5132dc2859 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -14,6 +14,7 @@ using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Textures;
+using osu.Framework.Graphics.Visualisation;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
@@ -23,6 +24,7 @@ using osuTK.Graphics.ES30;
namespace osu.Game.Rulesets.Osu.UI.Cursor
{
+ [DrawVisualiserHidden]
public partial class CursorTrail : Drawable, IRequireHighFrequencyMousePosition
{
private const int max_sprites = 2048;
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
index 0bb316e0aa..c2f7d84f5e 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
@@ -38,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public IBindable CursorScale => cursorScale;
+ ///
+ /// Mods which want to adjust cursor size should do so via this bindable.
+ ///
+ public readonly Bindable ModScaleAdjust = new Bindable(1);
+
private readonly Bindable cursorScale = new BindableFloat(1);
private Bindable userCursorScale = null!;
@@ -67,6 +72,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize);
autoCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale();
+ ModScaleAdjust.ValueChanged += _ => cursorScale.Value = CalculateCursorScale();
+
cursorScale.BindValueChanged(e => cursorScaleContainer.Scale = new Vector2(e.NewValue), true);
}
@@ -90,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected virtual float CalculateCursorScale()
{
- float scale = userCursorScale.Value;
+ float scale = userCursorScale.Value * ModScaleAdjust.Value;
if (autoCursorScale.Value && state != null)
{
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
index 9f63e84867..25428c8b2f 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
@@ -3,6 +3,7 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;
@@ -11,26 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{
public class ColourEvaluator
{
- ///
- /// A sigmoid function. It gives a value between (middle - height/2) and (middle + height/2).
- ///
- /// The input value.
- /// The center of the sigmoid, where the largest gradient occurs and value is equal to middle.
- /// The radius of the sigmoid, outside of which values are near the minimum/maximum.
- /// The middle of the sigmoid output.
- /// The height of the sigmoid output. This will be equal to max value - min value.
- private static double sigmoid(double val, double center, double width, double middle, double height)
- {
- double sigmoid = Math.Tanh(Math.E * -(val - center) / width);
- return sigmoid * (height / 2) + middle;
- }
-
///
/// Evaluate the difficulty of the first note of a .
///
public static double EvaluateDifficultyOf(MonoStreak monoStreak)
{
- return sigmoid(monoStreak.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
+ return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
}
///
@@ -38,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
///
public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern)
{
- return sigmoid(alternatingMonoPattern.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
+ return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
}
///
@@ -46,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
///
public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern)
{
- return 2 * (1 - sigmoid(repeatingHitPattern.RepetitionInterval, 2, 2, 0.5, 1));
+ return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
}
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index 64ea9d88cd..4185b67f4c 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -84,8 +84,11 @@ namespace osu.Game.Rulesets.Taiko.UI
protected virtual double ComputeTimeRange()
{
- // Adjust when we're using constant algorithm to not be sluggish.
- double multiplier = VisualisationMethod == ScrollVisualisationMethod.Constant ? 4 * Beatmap.Difficulty.SliderMultiplier : 1;
+ // Using the constant algorithm results in a sluggish scroll speed that's equal to 60 BPM.
+ // We need to adjust it to the expected default scroll speed (BPM * base SV multiplier).
+ double multiplier = VisualisationMethod == ScrollVisualisationMethod.Constant
+ ? (Beatmap.BeatmapInfo.BPM * Beatmap.Difficulty.SliderMultiplier) / 60
+ : 1;
return PlayfieldAdjustmentContainer.ComputeTimeRange() / multiplier;
}
diff --git a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs
new file mode 100644
index 0000000000..9947def06d
--- /dev/null
+++ b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs
@@ -0,0 +1,63 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Tests.Resources;
+using osu.Game.Tests.Visual;
+using MemoryStream = System.IO.MemoryStream;
+
+namespace osu.Game.Tests.Beatmaps.IO
+{
+ [HeadlessTest]
+ public partial class LegacyBeatmapExporterTest : OsuTestScene
+ {
+ [Resolved]
+ private BeatmapManager beatmaps { get; set; } = null!;
+
+ [Test]
+ public void TestObjectsSnappedAfterTruncatingExport()
+ {
+ IWorkingBeatmap beatmap = null!;
+ MemoryStream outStream = null!;
+
+ // Ensure importer encoding is correct
+ AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"decimal-timing-beatmap.olz"));
+ AddAssert("timing point has decimal offset", () => beatmap.Beatmap.ControlPointInfo.TimingPoints[0].Time, () => Is.EqualTo(284.725).Within(0.001));
+ AddAssert("kiai has decimal offset", () => beatmap.Beatmap.ControlPointInfo.EffectPoints[0].Time, () => Is.EqualTo(28520.019).Within(0.001));
+ AddAssert("hit object has decimal offset", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28520.019).Within(0.001));
+
+ // Ensure exporter legacy conversion is correct
+ AddStep("export", () =>
+ {
+ outStream = new MemoryStream();
+
+ new LegacyBeatmapExporter(LocalStorage)
+ .ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
+ });
+
+ AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
+ AddAssert("timing point has truncated offset", () => beatmap.Beatmap.ControlPointInfo.TimingPoints[0].Time, () => Is.EqualTo(284).Within(0.001));
+ AddAssert("kiai is snapped", () => beatmap.Beatmap.ControlPointInfo.EffectPoints[0].Time, () => Is.EqualTo(28519).Within(0.001));
+ AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001));
+ }
+
+ private IWorkingBeatmap importBeatmapFromStream(Stream stream)
+ {
+ var imported = beatmaps.Import(new ImportTask(stream, "filename.osz")).GetResultSafely();
+ return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0]));
+ }
+
+ private IWorkingBeatmap importBeatmapFromArchives(string filename)
+ {
+ var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
+ return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0]));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Resources/Archives/decimal-timing-beatmap.olz b/osu.Game.Tests/Resources/Archives/decimal-timing-beatmap.olz
new file mode 100644
index 0000000000..38dedc35d1
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/decimal-timing-beatmap.olz differ
diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
index 747cf73baf..56e7b4d39f 100644
--- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
+++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
@@ -205,7 +205,9 @@ namespace osu.Game.Tests.Visual.Collections
AddStep("click first delete button", () =>
{
- InputManager.MoveMouseTo(dialog.ChildrenOfType().First(), new Vector2(5, 0));
+ InputManager.MoveMouseTo(dialog
+ .ChildrenOfType().Single(i => i.Model.Value.Name == "1")
+ .ChildrenOfType().Single(), new Vector2(5, 0));
InputManager.Click(MouseButton.Left);
});
@@ -213,9 +215,11 @@ namespace osu.Game.Tests.Visual.Collections
assertCollectionCount(1);
assertCollectionName(0, "2");
- AddStep("click first delete button", () =>
+ AddStep("click second delete button", () =>
{
- InputManager.MoveMouseTo(dialog.ChildrenOfType().First(), new Vector2(5, 0));
+ InputManager.MoveMouseTo(dialog
+ .ChildrenOfType().Single(i => i.Model.Value.Name == "2")
+ .ChildrenOfType().Single(), new Vector2(5, 0));
InputManager.Click(MouseButton.Left);
});
@@ -261,6 +265,7 @@ namespace osu.Game.Tests.Visual.Collections
}
[Test]
+ [Solo]
public void TestCollectionRenamedExternal()
{
BeatmapCollection first = null!;
@@ -310,7 +315,7 @@ namespace osu.Game.Tests.Visual.Collections
AddStep("focus first collection", () =>
{
- InputManager.MoveMouseTo(firstItem = dialog.ChildrenOfType().First());
+ InputManager.MoveMouseTo(firstItem = dialog.ChildrenOfType().Single(i => i.Model.Value.Name == "1"));
InputManager.Click(MouseButton.Left);
});
@@ -337,6 +342,6 @@ namespace osu.Game.Tests.Visual.Collections
=> AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType().Count() == count + 1); // +1 for placeholder
private void assertCollectionName(int index, string name)
- => AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType().ElementAt(index).ChildrenOfType().First().Text == name);
+ => AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType().Single().OrderedItems.ElementAt(index).ChildrenOfType().First().Text == name);
}
}
diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs
index f1a2d6b5f2..7619328e68 100644
--- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs
+++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
Add(metadataClient);
// add button to observe for daily challenge changes and perform its logic.
- Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D));
+ Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D));
}
[Test]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
index c7f1eabab2..0f47c3cd27 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
@@ -1,14 +1,18 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Tests.Resources;
@@ -52,6 +56,39 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
}
+ [Test]
+ public void TestNotEnoughTimedHitEvents()
+ {
+ AddStep("Set short reference score", () =>
+ {
+ List hitEvents =
+ [
+ // 10 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows
+ new HitEvent(30, 1, HitResult.LargeTickHit, new SliderHeadCircle { ClassicSliderBehaviour = true }, null, null),
+ new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
+ new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
+ new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
+ new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
+ new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
+ new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
+ new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
+ new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
+ new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
+ ];
+
+ foreach (var ev in hitEvents)
+ ev.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ offsetControl.ReferenceScore.Value = new ScoreInfo
+ {
+ HitEvents = hitEvents,
+ BeatmapInfo = Beatmap.Value.BeatmapInfo,
+ };
+ });
+
+ AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
+ }
+
[Test]
public void TestScoreFromDifferentBeatmap()
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index 1949808dfe..cf813cfd51 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -523,7 +523,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("restart completed", () => getCurrentPlayer() != null && getCurrentPlayer() != previousPlayer);
AddStep("release quick retry key", () => InputManager.ReleaseKey(Key.Tilde));
- AddUntilStep("wait for player", () => getCurrentPlayer()?.LoadState == LoadState.Ready);
+ AddUntilStep("wait for player", () => getCurrentPlayer()?.LoadState >= LoadState.Ready);
AddUntilStep("time reached zero", () => getCurrentPlayer()?.GameplayClockContainer.CurrentTime > 0);
AddUntilStep("skip button not visible", () => !checkSkipButtonVisible());
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
index aff6139c08..4f1a63341a 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
@@ -223,7 +223,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected partial class OutroPlayer : TestPlayer
{
- public void ExitViaPause() => PerformExit(true);
+ public void ExitViaPause() => PerformExitWithConfirmation();
public new FailOverlay FailOverlay => base.FailOverlay;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
index 4961aabbb2..365dce551c 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
@@ -63,21 +63,21 @@ namespace osu.Game.Tests.Visual.Online
{
Username = @"flyte",
Id = 3103765,
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
+ CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
}),
new UserBrickPanel(new APIUser
{
Username = @"peppy",
Id = 2,
Colour = "99EB47",
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
}),
new UserGridPanel(new APIUser
{
Username = @"flyte",
Id = 3103765,
CountryCode = CountryCode.JP,
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg",
+ CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
Status = { Value = UserStatus.Online }
}) { Width = 300 },
boundPanel1 = new UserGridPanel(new APIUser
@@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.Online
Username = @"peppy",
Id = 2,
CountryCode = CountryCode.AU,
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsSupporter = true,
SupportLevel = 3,
}) { Width = 300 },
@@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Online
Username = @"flyte",
Id = 3103765,
CountryCode = CountryCode.JP,
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg",
+ CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 }
}) { Width = 300 },
new UserRankPanel(new APIUser
@@ -112,7 +112,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 2,
Colour = "99EB47",
CountryCode = CountryCode.AU,
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
}) { Width = 300 }
}
@@ -176,6 +176,14 @@ namespace osu.Game.Tests.Visual.Online
CountryRank = RNG.Next(100000)
}, Ruleset.Value);
});
+ AddStep("set statistics to something big", () =>
+ {
+ statisticsProvider.UpdateStatistics(new UserStatistics
+ {
+ GlobalRank = RNG.Next(1_000_000, 100_000_000),
+ CountryRank = RNG.Next(1_000_000, 100_000_000)
+ }, Ruleset.Value);
+ });
AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs
index 3e38b66029..760210c370 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs
@@ -40,6 +40,13 @@ namespace osu.Game.Tests.Visual.Ranking
AddSliderStep("height", 0.0f, 1000.0f, height.Value, height.Set);
}
+ [Test]
+ public void TestZeroEvents()
+ {
+ createTest(new List());
+ AddStep("update offset", () => graph.UpdateOffset(10));
+ }
+
[Test]
public void TestManyDistributedEventsOffset()
{
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
index fd102da026..d8573b2d03 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
@@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
-using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -23,7 +20,6 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
-using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko;
@@ -35,15 +31,11 @@ namespace osu.Game.Tests.Visual.SongSelect
[TestFixture]
public partial class TestSceneBeatmapInfoWedge : OsuTestScene
{
- private RulesetStore rulesets;
- private TestBeatmapInfoWedge infoWedge;
- private readonly List beatmaps = new List();
+ [Resolved]
+ private RulesetStore rulesets { get; set; } = null!;
- [BackgroundDependencyLoader]
- private void load(RulesetStore rulesets)
- {
- this.rulesets = rulesets;
- }
+ private TestBeatmapInfoWedge infoWedge = null!;
+ private readonly List beatmaps = new List();
protected override void LoadComplete()
{
@@ -156,7 +148,7 @@ namespace osu.Game.Tests.Visual.SongSelect
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
- OsuModDoubleTime doubleTime = null;
+ OsuModDoubleTime doubleTime = null!;
selectBeatmap(beatmap);
checkDisplayedBPM($"{bpm}");
@@ -173,7 +165,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[TestCase(120, 120.4, null, "120")]
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
[TestCase(120, 120.4, "DT", "180")]
- public void TestVaryingBPM(double commonBpm, double otherBpm, string mod, string expectedDisplay)
+ public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
{
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
@@ -203,7 +195,7 @@ namespace osu.Game.Tests.Visual.SongSelect
double drain = beatmap.CalculateDrainLength();
beatmap.BeatmapInfo.Length = drain;
- OsuModDoubleTime doubleTime = null;
+ OsuModDoubleTime doubleTime = null!;
selectBeatmap(beatmap);
checkDisplayedLength(drain);
@@ -221,14 +213,15 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep($"check map drain ({displayedLength})", () =>
{
- var label = infoWedge.DisplayedContent.ChildrenOfType().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength));
+ var label = infoWedge.DisplayedContent.ChildrenOfType()
+ .Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength));
return label.Statistic.Content == displayedLength.ToString();
});
}
private void setRuleset(RulesetInfo rulesetInfo)
{
- Container containerBefore = null;
+ Container? containerBefore = null;
AddStep("set ruleset", () =>
{
@@ -242,9 +235,9 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
}
- private void selectBeatmap([CanBeNull] IBeatmap b)
+ private void selectBeatmap(IBeatmap? b)
{
- Container containerBefore = null;
+ Container? containerBefore = null;
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
{
@@ -307,11 +300,6 @@ namespace osu.Game.Tests.Visual.SongSelect
public new WedgeInfoText Info => base.Info;
}
- private class TestHitObject : ConvertHitObject, IHasPosition
- {
- public float X => 0;
- public float Y => 0;
- public Vector2 Position { get; } = Vector2.Zero;
- }
+ private class TestHitObject : ConvertHitObject;
}
}
diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs
index 35bd4ee958..fbbab3a604 100644
--- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs
+++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs
@@ -14,9 +14,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
-using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Select;
-using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2
{
@@ -209,11 +207,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
public new WedgeInfoText? Info => base.Info;
}
- private class TestHitObject : ConvertHitObject, IHasPosition
- {
- public float X => 0;
- public float Y => 0;
- public Vector2 Position { get; } = Vector2.Zero;
- }
+ private class TestHitObject : ConvertHitObject;
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs
index 91525e02c1..e3a6fca319 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs
@@ -159,6 +159,23 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("check star rating is 2", getText, () => Is.EqualTo("Star Rating: 2.00"));
}
+ [Test]
+ public void TestMaxPp()
+ {
+ AddStep("set test ruleset", () => Ruleset.Value = new TestRuleset().RulesetInfo);
+ AddStep("set max pp attribute", () => text.Attribute.Value = BeatmapAttribute.MaxPP);
+ AddAssert("check max pp is 0", getText, () => Is.EqualTo("Max PP: 0"));
+
+ // Adding mod
+ TestMod mod = null!;
+ AddStep("add mod with pp 1", () => SelectedMods.Value = new[] { mod = new TestMod { Performance = { Value = 1 } } });
+ AddUntilStep("check max pp is 1", getText, () => Is.EqualTo("Max PP: 1"));
+
+ // Changing mod setting
+ AddStep("change mod pp to 2", () => mod.Performance.Value = 2);
+ AddUntilStep("check max pp is 2", getText, () => Is.EqualTo("Max PP: 2"));
+ }
+
private string getText() => text.ChildrenOfType().Single().Text.ToString();
private class TestRuleset : Ruleset
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs
index 8f72be37df..d8baca6d23 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs
@@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.UserInterface
break;
case Key.Q:
- buttons.OnExit = action;
+ buttons.OnExit = _ => action();
break;
case Key.O:
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs
index 41543669eb..4925facd8a 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestStandardButton()
{
AddStep("add button", () => Child = new MainMenuButton(
- ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => { }, 0, Key.P)
+ ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.P)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)],
- Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
+ Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -161,7 +161,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)],
- Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
+ Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
index 871faf5906..fc4175415c 100644
--- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
@@ -4,12 +4,15 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
+using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
+using osu.Framework.Graphics.Textures;
using osu.Framework.Lists;
using osu.Framework.Logging;
using osu.Framework.Threading;
@@ -18,7 +21,11 @@ using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
+using osu.Game.Scoring;
+using osu.Game.Skinning;
+using osu.Game.Storyboards;
namespace osu.Game.Beatmaps
{
@@ -237,10 +244,37 @@ namespace osu.Game.Beatmaps
var ruleset = rulesetInfo.CreateInstance();
Debug.Assert(ruleset != null);
- var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo));
- var attributes = calculator.Calculate(key.OrderedMods, cancellationToken);
+ PlayableCachedWorkingBeatmap workingBeatmap = new PlayableCachedWorkingBeatmap(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo));
+ IBeatmap playableBeatmap = workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, key.OrderedMods, cancellationToken);
- return new StarDifficulty(attributes);
+ var difficulty = ruleset.CreateDifficultyCalculator(workingBeatmap).Calculate(key.OrderedMods, cancellationToken);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var performanceCalculator = ruleset.CreatePerformanceCalculator();
+ if (performanceCalculator == null)
+ return new StarDifficulty(difficulty, new PerformanceAttributes());
+
+ ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
+ scoreProcessor.Mods.Value = key.OrderedMods;
+ scoreProcessor.ApplyBeatmap(playableBeatmap);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ ScoreInfo perfectScore = new ScoreInfo(key.BeatmapInfo, ruleset.RulesetInfo)
+ {
+ Passed = true,
+ Accuracy = 1,
+ Mods = key.OrderedMods,
+ MaxCombo = scoreProcessor.MaximumCombo,
+ Combo = scoreProcessor.MaximumCombo,
+ TotalScore = scoreProcessor.MaximumTotalScore,
+ Statistics = scoreProcessor.MaximumStatistics,
+ MaximumStatistics = scoreProcessor.MaximumStatistics
+ };
+
+ var performance = performanceCalculator.Calculate(perfectScore, difficulty);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return new StarDifficulty(difficulty, performance);
}
catch (OperationCanceledException)
{
@@ -276,7 +310,6 @@ namespace osu.Game.Beatmaps
{
public readonly BeatmapInfo BeatmapInfo;
public readonly RulesetInfo Ruleset;
-
public readonly Mod[] OrderedMods;
public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo? ruleset, IEnumerable? mods)
@@ -317,5 +350,42 @@ namespace osu.Game.Beatmaps
CancellationToken = cancellationToken;
}
}
+
+ ///
+ /// A working beatmap that caches its playable representation.
+ /// This is intended as single-use for when it is guaranteed that the playable beatmap can be reused.
+ ///
+ private class PlayableCachedWorkingBeatmap : IWorkingBeatmap
+ {
+ private readonly IWorkingBeatmap working;
+ private IBeatmap? playable;
+
+ public PlayableCachedWorkingBeatmap(IWorkingBeatmap working)
+ {
+ this.working = working;
+ }
+
+ public IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList mods)
+ => playable ??= working.GetPlayableBeatmap(ruleset, mods);
+
+ public IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList mods, CancellationToken cancellationToken)
+ => playable ??= working.GetPlayableBeatmap(ruleset, mods, cancellationToken);
+
+ IBeatmapInfo IWorkingBeatmap.BeatmapInfo => working.BeatmapInfo;
+ bool IWorkingBeatmap.BeatmapLoaded => working.BeatmapLoaded;
+ bool IWorkingBeatmap.TrackLoaded => working.TrackLoaded;
+ IBeatmap IWorkingBeatmap.Beatmap => working.Beatmap;
+ Texture IWorkingBeatmap.GetBackground() => working.GetBackground();
+ Texture IWorkingBeatmap.GetPanelBackground() => working.GetPanelBackground();
+ Waveform IWorkingBeatmap.Waveform => working.Waveform;
+ Storyboard IWorkingBeatmap.Storyboard => working.Storyboard;
+ ISkin IWorkingBeatmap.Skin => working.Skin;
+ Track IWorkingBeatmap.Track => working.Track;
+ Track IWorkingBeatmap.LoadTrack() => working.LoadTrack();
+ Stream IWorkingBeatmap.GetStream(string storagePath) => working.GetStream(storagePath);
+ void IWorkingBeatmap.BeginAsyncLoad() => working.BeginAsyncLoad();
+ void IWorkingBeatmap.CancelAsyncLoad() => working.CancelAsyncLoad();
+ void IWorkingBeatmap.PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint) => working.PrepareTrackForPreview(looping, offsetFromPreviewPoint);
+ }
}
}
diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs
index 36ddb6030e..8182fe24b2 100644
--- a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs
+++ b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
@@ -146,7 +147,8 @@ namespace osu.Game.Beatmaps.Drawables
approachRate.Text = @" AR: " + adjustedDifficulty.ApproachRate.ToString(@"0.##");
overallDifficulty.Text = @" OD: " + adjustedDifficulty.OverallDifficulty.ToString(@"0.##");
- length.Text = "Length: " + TimeSpan.FromMilliseconds(displayedContent.BeatmapInfo.Length / rate).ToString(@"mm\:ss");
+ TimeSpan lengthTimeSpan = TimeSpan.FromMilliseconds(displayedContent.BeatmapInfo.Length / rate);
+ length.Text = "Length: " + lengthTimeSpan.ToFormattedDuration();
bpm.Text = " BPM: " + Math.Round(bpmAdjusted, 0);
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index b068c87fbb..3d8c8a6e7a 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -38,8 +38,7 @@ namespace osu.Game.Beatmaps.Formats
internal static RulesetStore? RulesetStore;
private Beatmap beatmap = null!;
-
- private ConvertHitObjectParser? parser;
+ private ConvertHitObjectParser parser = null!;
private LegacySampleBank defaultSampleBank;
private int defaultSampleVolume = 100;
@@ -80,6 +79,7 @@ namespace osu.Game.Beatmaps.Formats
{
this.beatmap = beatmap;
this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion;
+ parser = new ConvertHitObjectParser(getOffsetTime(), FormatVersion);
applyLegacyDefaults(this.beatmap.BeatmapInfo);
@@ -162,7 +162,8 @@ namespace osu.Game.Beatmaps.Formats
{
if (hitObject is IHasRepeats hasRepeats)
{
- SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1) ?? SampleControlPoint.DEFAULT;
+ SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1)
+ ?? SampleControlPoint.DEFAULT;
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
@@ -175,7 +176,8 @@ namespace osu.Game.Beatmaps.Formats
}
else
{
- SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY) ?? SampleControlPoint.DEFAULT;
+ SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY)
+ ?? SampleControlPoint.DEFAULT;
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
}
}
@@ -263,29 +265,7 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"Mode":
- int rulesetID = Parsing.ParseInt(pair.Value);
-
- beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally.");
-
- switch (rulesetID)
- {
- case 0:
- parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
- break;
-
- case 1:
- parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
- break;
-
- case 2:
- parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
- break;
-
- case 3:
- parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
- break;
- }
-
+ beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(Parsing.ParseInt(pair.Value)) ?? throw new ArgumentException("Ruleset is not available locally.");
break;
case @"LetterboxInBreaks":
@@ -617,17 +597,10 @@ namespace osu.Game.Beatmaps.Formats
private void handleHitObject(string line)
{
- // If the ruleset wasn't specified, assume the osu!standard ruleset.
- parser ??= new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
-
var obj = parser.Parse(line);
+ obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
- if (obj != null)
- {
- obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
-
- beatmap.HitObjects.Add(obj);
- }
+ beatmap.HitObjects.Add(obj);
}
private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0);
diff --git a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs
index 07f170f996..6fab66bf70 100644
--- a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs
+++ b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs
@@ -6,7 +6,7 @@ using System;
namespace osu.Game.Beatmaps.Legacy
{
[Flags]
- internal enum LegacyHitObjectType
+ public enum LegacyHitObjectType
{
Circle = 1,
Slider = 1 << 1,
diff --git a/osu.Game/Beatmaps/StarDifficulty.cs b/osu.Game/Beatmaps/StarDifficulty.cs
index 6aac275a6a..9f7a92fe46 100644
--- a/osu.Game/Beatmaps/StarDifficulty.cs
+++ b/osu.Game/Beatmaps/StarDifficulty.cs
@@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using JetBrains.Annotations;
using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty;
@@ -25,30 +22,34 @@ namespace osu.Game.Beatmaps
/// The difficulty attributes computed for the given beatmap.
/// Might not be available if the star difficulty is associated with a beatmap that's not locally available.
///
- [CanBeNull]
- public readonly DifficultyAttributes Attributes;
+ public readonly DifficultyAttributes? DifficultyAttributes;
///
- /// Creates a structure based on computed
- /// by a .
+ /// The performance attributes computed for a perfect score on the given beatmap.
+ /// Might not be available if the star difficulty is associated with a beatmap that's not locally available.
///
- public StarDifficulty([NotNull] DifficultyAttributes attributes)
+ public readonly PerformanceAttributes? PerformanceAttributes;
+
+ ///
+ /// Creates a structure.
+ ///
+ public StarDifficulty(DifficultyAttributes difficulty, PerformanceAttributes performance)
{
- Stars = double.IsFinite(attributes.StarRating) ? attributes.StarRating : 0;
- MaxCombo = attributes.MaxCombo;
- Attributes = attributes;
+ Stars = double.IsFinite(difficulty.StarRating) ? difficulty.StarRating : 0;
+ MaxCombo = difficulty.MaxCombo;
+ DifficultyAttributes = difficulty;
+ PerformanceAttributes = performance;
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
}
///
/// Creates a structure with a pre-populated star difficulty and max combo
- /// in scenarios where computing is not feasible (i.e. when working with online sources).
+ /// in scenarios where computing is not feasible (i.e. when working with online sources).
///
public StarDifficulty(double starDifficulty, int maxCombo)
{
Stars = double.IsFinite(starDifficulty) ? starDifficulty : 0;
MaxCombo = maxCombo;
- Attributes = null;
}
public DifficultyRating DifficultyRating => GetDifficultyRating(Stars);
diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs
index 6fe38a3229..164ec558a4 100644
--- a/osu.Game/Collections/DrawableCollectionList.cs
+++ b/osu.Game/Collections/DrawableCollectionList.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
@@ -29,7 +30,11 @@ namespace osu.Game.Collections
private IDisposable? realmSubscription;
- protected override FillFlowContainer>> CreateListFillFlowContainer() => new Flow
+ private Flow flow = null!;
+
+ public IEnumerable OrderedItems => flow.FlowingChildren;
+
+ protected override FillFlowContainer>> CreateListFillFlowContainer() => flow = new Flow
{
DragActive = { BindTarget = DragActive }
};
@@ -43,8 +48,25 @@ namespace osu.Game.Collections
private void collectionsChanged(IRealmCollection collections, ChangeSet? changes)
{
- Items.Clear();
- Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm)));
+ if (changes == null)
+ {
+ Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm)));
+ return;
+ }
+
+ foreach (int i in changes.DeletedIndices.OrderDescending())
+ Items.RemoveAt(i);
+
+ foreach (int i in changes.InsertedIndices)
+ Items.Insert(i, collections[i].ToLive(realm));
+
+ foreach (int i in changes.NewModifiedIndices)
+ {
+ var updatedItem = collections[i];
+
+ Items.RemoveAt(i);
+ Items.Insert(i, updatedItem.ToLive(realm));
+ }
}
protected override OsuRearrangeableListItem> CreateOsuDrawable(Live item)
@@ -123,12 +145,37 @@ namespace osu.Game.Collections
var previous = PlaceholderItem;
placeholderContainer.Clear(false);
- placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new BeatmapCollection().ToLiveUnmanaged(), false));
+ placeholderContainer.Add(PlaceholderItem = new NewCollectionEntryItem());
return previous;
}
}
+ private partial class NewCollectionEntryItem : DrawableCollectionListItem
+ {
+ [Resolved]
+ private RealmAccess realm { get; set; } = null!;
+
+ public NewCollectionEntryItem()
+ : base(new BeatmapCollection().ToLiveUnmanaged(), false)
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ TextBox.OnCommit += (sender, newText) =>
+ {
+ if (string.IsNullOrEmpty(TextBox.Text))
+ return;
+
+ realm.Write(r => r.Add(new BeatmapCollection(TextBox.Text)));
+ TextBox.Text = string.Empty;
+ };
+ }
+ }
+
///
/// The flow of . Disables layout easing unless a drag is in progress.
///
diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs
index e71368c079..f07ec87353 100644
--- a/osu.Game/Collections/DrawableCollectionListItem.cs
+++ b/osu.Game/Collections/DrawableCollectionListItem.cs
@@ -28,6 +28,10 @@ namespace osu.Game.Collections
private const float item_height = 35;
private const float button_width = item_height * 0.75f;
+ protected TextBox TextBox => content.TextBox;
+
+ private ItemContent content = null!;
+
///
/// Creates a new .
///
@@ -48,7 +52,7 @@ namespace osu.Game.Collections
CornerRadius = item_height / 2;
}
- protected override Drawable CreateContent() => new ItemContent(Model);
+ protected override Drawable CreateContent() => content = new ItemContent(Model);
///
/// The main content of the .
@@ -57,10 +61,7 @@ namespace osu.Game.Collections
{
private readonly Live collection;
- private ItemTextBox textBox = null!;
-
- [Resolved]
- private RealmAccess realm { get; set; } = null!;
+ public ItemTextBox TextBox { get; private set; } = null!;
public ItemContent(Live collection)
{
@@ -80,7 +81,7 @@ namespace osu.Game.Collections
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
- IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v)
+ IsTextBoxHovered = v => TextBox.ReceivePositionalInputAt(v)
}
: Empty(),
new Container
@@ -89,7 +90,7 @@ namespace osu.Game.Collections
Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 },
Children = new Drawable[]
{
- textBox = new ItemTextBox
+ TextBox = new ItemTextBox
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
@@ -107,18 +108,14 @@ namespace osu.Game.Collections
base.LoadComplete();
// Bind late, as the collection name may change externally while still loading.
- textBox.Current.Value = collection.PerformRead(c => c.IsValid ? c.Name : string.Empty);
- textBox.OnCommit += onCommit;
+ TextBox.Current.Value = collection.PerformRead(c => c.IsValid ? c.Name : string.Empty);
+ TextBox.OnCommit += onCommit;
}
private void onCommit(TextBox sender, bool newText)
{
- if (collection.IsManaged)
- collection.PerformWrite(c => c.Name = textBox.Current.Value);
- else if (!string.IsNullOrEmpty(textBox.Current.Value))
- realm.Write(r => r.Add(new BeatmapCollection(textBox.Current.Value)));
-
- textBox.Text = string.Empty;
+ if (collection.IsManaged && collection.Value.Name != TextBox.Current.Value)
+ collection.PerformWrite(c => c.Name = TextBox.Current.Value);
}
}
diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs
index 17c2c8c88d..eb48425588 100644
--- a/osu.Game/Database/LegacyBeatmapExporter.cs
+++ b/osu.Game/Database/LegacyBeatmapExporter.cs
@@ -59,7 +59,25 @@ namespace osu.Game.Database
};
// Convert beatmap elements to be compatible with legacy format
- // So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves
+ // So we truncate time and position values to integers, and convert paths with multiple segments to Bézier curves
+
+ // We must first truncate all timing points and move all objects in the timing section with it to ensure everything stays snapped
+ for (int i = 0; i < playableBeatmap.ControlPointInfo.TimingPoints.Count; i++)
+ {
+ var timingPoint = playableBeatmap.ControlPointInfo.TimingPoints[i];
+ double offset = Math.Floor(timingPoint.Time) - timingPoint.Time;
+ double nextTimingPointTime = i + 1 < playableBeatmap.ControlPointInfo.TimingPoints.Count
+ ? playableBeatmap.ControlPointInfo.TimingPoints[i + 1].Time
+ : double.PositiveInfinity;
+
+ // Offset all control points in the timing section (including the current one)
+ foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints.Where(o => o.Time >= timingPoint.Time && o.Time < nextTimingPointTime))
+ controlPoint.Time += offset;
+
+ // Offset all hit objects in the timing section
+ foreach (var hitObject in playableBeatmap.HitObjects.Where(o => o.StartTime >= timingPoint.Time && o.StartTime < nextTimingPointTime))
+ hitObject.StartTime += offset;
+ }
foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints)
controlPoint.Time = Math.Floor(controlPoint.Time);
diff --git a/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs b/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs
index b2e2285faf..390a6f9ca4 100644
--- a/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs
+++ b/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs
@@ -12,23 +12,28 @@ namespace osu.Game.Localisation.SkinComponents
///
/// "Attribute"
///
- public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), "Attribute");
+ public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), @"Attribute");
///
/// "The attribute to be displayed."
///
- public static LocalisableString AttributeDescription => new TranslatableString(getKey(@"attribute_description"), "The attribute to be displayed.");
+ public static LocalisableString AttributeDescription => new TranslatableString(getKey(@"attribute_description"), @"The attribute to be displayed.");
///
/// "Template"
///
- public static LocalisableString Template => new TranslatableString(getKey(@"template"), "Template");
+ public static LocalisableString Template => new TranslatableString(getKey(@"template"), @"Template");
///
/// "Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)."
///
public static LocalisableString TemplateDescription => new TranslatableString(getKey(@"template_description"), @"Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values).");
- private static string getKey(string key) => $"{prefix}:{key}";
+ ///
+ /// "Max PP"
+ ///
+ public static LocalisableString MaxPP => new TranslatableString(getKey(@"max_pp"), @"Max PP");
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
}
}
diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
deleted file mode 100644
index 946d83b14b..0000000000
--- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using JetBrains.Annotations;
-using osu.Framework.Extensions.ObjectExtensions;
-using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Scoring;
-using osu.Game.Scoring;
-
-namespace osu.Game.Rulesets.Difficulty
-{
- public class PerformanceBreakdownCalculator
- {
- private readonly IBeatmap playableBeatmap;
- private readonly BeatmapDifficultyCache difficultyCache;
-
- public PerformanceBreakdownCalculator(IBeatmap playableBeatmap, BeatmapDifficultyCache difficultyCache)
- {
- this.playableBeatmap = playableBeatmap;
- this.difficultyCache = difficultyCache;
- }
-
- [ItemCanBeNull]
- public async Task CalculateAsync(ScoreInfo score, CancellationToken cancellationToken = default)
- {
- var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
-
- var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator();
-
- // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
- if (attributes?.Attributes == null || performanceCalculator == null)
- return null;
-
- cancellationToken.ThrowIfCancellationRequested();
-
- PerformanceAttributes[] performanceArray = await Task.WhenAll(
- // compute actual performance
- performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken),
- // compute performance for perfect play
- getPerfectPerformance(score, cancellationToken)
- ).ConfigureAwait(false);
-
- return new PerformanceBreakdown(performanceArray[0] ?? new PerformanceAttributes(), performanceArray[1] ?? new PerformanceAttributes());
- }
-
- [ItemCanBeNull]
- private Task getPerfectPerformance(ScoreInfo score, CancellationToken cancellationToken = default)
- {
- return Task.Run(async () =>
- {
- Ruleset ruleset = score.Ruleset.CreateInstance();
- ScoreInfo perfectPlay = score.DeepClone();
- perfectPlay.Accuracy = 1;
- perfectPlay.Passed = true;
-
- // calculate max combo
- // todo: Get max combo from difficulty calculator instead when diffcalc properly supports lazer-first scores
- perfectPlay.MaxCombo = calculateMaxCombo(playableBeatmap);
-
- // create statistics assuming all hit objects have perfect hit result
- var statistics = playableBeatmap.HitObjects
- .SelectMany(getPerfectHitResults)
- .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count()))
- .ToDictionary(pair => pair.hitResult, pair => pair.count);
- perfectPlay.Statistics = statistics;
- perfectPlay.MaximumStatistics = statistics;
-
- // calculate total score
- ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
- scoreProcessor.Mods.Value = perfectPlay.Mods;
- scoreProcessor.ApplyBeatmap(playableBeatmap);
- perfectPlay.TotalScore = scoreProcessor.MaximumTotalScore;
-
- // compute rank achieved
- // default to SS, then adjust the rank with mods
- perfectPlay.Rank = ScoreRank.X;
-
- foreach (IApplicableToScoreProcessor mod in perfectPlay.Mods.OfType())
- {
- perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1);
- }
-
- // calculate performance for this perfect score
- var difficulty = await difficultyCache.GetDifficultyAsync(
- playableBeatmap.BeatmapInfo,
- score.Ruleset,
- score.Mods,
- cancellationToken
- ).ConfigureAwait(false);
-
- var performanceCalculator = ruleset.CreatePerformanceCalculator();
-
- if (performanceCalculator == null || difficulty == null)
- return null;
-
- return await performanceCalculator.CalculateAsync(perfectPlay, difficulty.Value.Attributes.AsNonNull(), cancellationToken).ConfigureAwait(false);
- }, cancellationToken);
- }
-
- private int calculateMaxCombo(IBeatmap beatmap)
- {
- return beatmap.HitObjects.SelectMany(getPerfectHitResults).Count(r => r.AffectsCombo());
- }
-
- private IEnumerable getPerfectHitResults(HitObject hitObject)
- {
- foreach (HitObject nested in hitObject.NestedHitObjects)
- yield return nested.Judgement.MaxResult;
-
- yield return hitObject.Judgement.MaxResult;
- }
- }
-}
diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
index 1cb6b69f91..3ba67793dc 100644
--- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
+++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
@@ -74,6 +74,10 @@ namespace osu.Game.Rulesets.Difficulty.Skills
return 0.0;
double consistentTopStrain = DifficultyValue() / 10; // What would the top strain be if all strain values were identical
+
+ if (consistentTopStrain == 0)
+ return ObjectStrains.Count;
+
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88))));
}
diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
new file mode 100644
index 0000000000..b9efcd683d
--- /dev/null
+++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+
+namespace osu.Game.Rulesets.Difficulty.Utils
+{
+ public static class DifficultyCalculationUtils
+ {
+ ///
+ /// Converts BPM value into milliseconds
+ ///
+ /// Beats per minute
+ /// Which rhythm delimiter to use, default is 1/4
+ /// BPM conveted to milliseconds
+ public static double BPMToMilliseconds(double bpm, int delimiter = 4)
+ {
+ return 60000.0 / delimiter / bpm;
+ }
+
+ ///
+ /// Converts milliseconds value into a BPM value
+ ///
+ /// Milliseconds
+ /// Which rhythm delimiter to use, default is 1/4
+ /// Milliseconds conveted to beats per minute
+ public static double MillisecondsToBPM(double ms, int delimiter = 4)
+ {
+ return 60000.0 / (ms * delimiter);
+ }
+
+ ///
+ /// Calculates a S-shaped logistic function (https://en.wikipedia.org/wiki/Logistic_function)
+ ///
+ /// Value to calculate the function for
+ /// Maximum value returnable by the function
+ /// Growth rate of the function
+ /// How much the function midpoint is offset from zero
+ /// The output of logistic function of
+ public static double Logistic(double x, double midpointOffset, double multiplier, double maxValue = 1) => maxValue / (1 + Math.Exp(multiplier * (midpointOffset - x)));
+
+ ///
+ /// Calculates a S-shaped logistic function (https://en.wikipedia.org/wiki/Logistic_function)
+ ///
+ /// Maximum value returnable by the function
+ /// Exponent
+ /// The output of logistic function
+ public static double Logistic(double exponent, double maxValue = 1) => maxValue / (1 + Math.Exp(exponent));
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs
deleted file mode 100644
index 96c779e79b..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Objects.Types;
-using osuTK;
-
-namespace osu.Game.Rulesets.Objects.Legacy.Catch
-{
- ///
- /// Legacy osu!catch Hit-type, used for parsing Beatmaps.
- ///
- internal sealed class ConvertHit : ConvertHitObject, IHasPosition
- {
- public float X => Position.X;
-
- public float Y => Position.Y;
-
- public Vector2 Position { get; set; }
- }
-}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs
deleted file mode 100644
index a5c1a73fa7..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using osuTK;
-using osu.Game.Audio;
-using System.Collections.Generic;
-
-namespace osu.Game.Rulesets.Objects.Legacy.Catch
-{
- ///
- /// A HitObjectParser to parse legacy osu!catch Beatmaps.
- ///
- public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
- {
- private ConvertHitObject lastObject;
-
- public ConvertHitObjectParser(double offset, int formatVersion)
- : base(offset, formatVersion)
- {
- }
-
- protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
- {
- return lastObject = new ConvertHit
- {
- Position = position,
- NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo,
- ComboOffset = newCombo ? comboOffset : 0
- };
- }
-
- protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
- IList> nodeSamples)
- {
- return lastObject = new ConvertSlider
- {
- Position = position,
- NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo,
- ComboOffset = newCombo ? comboOffset : 0,
- Path = new SliderPath(controlPoints, length),
- NodeSamples = nodeSamples,
- RepeatCount = repeatCount
- };
- }
-
- protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
- {
- return lastObject = new ConvertSpinner
- {
- Duration = duration,
- NewCombo = newCombo
- // Spinners cannot have combo offset.
- };
- }
-
- protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
- {
- return lastObject = null;
- }
- }
-}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs
deleted file mode 100644
index bcf1c7fae2..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Objects.Types;
-using osuTK;
-
-namespace osu.Game.Rulesets.Objects.Legacy.Catch
-{
- ///
- /// Legacy osu!catch Slider-type, used for parsing Beatmaps.
- ///
- internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition
- {
- public float X => Position.X;
-
- public float Y => Position.Y;
-
- public Vector2 Position { get; set; }
- }
-}
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs
new file mode 100644
index 0000000000..d1852d7032
--- /dev/null
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Objects.Legacy
+{
+ ///
+ /// Legacy "HitCircle" hit object type.
+ ///
+ ///
+ /// Only used for parsing beatmaps and not gameplay.
+ ///
+ internal sealed class ConvertHitCircle : ConvertHitObject;
+}
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs
index bb36aab0b3..28683583ee 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs
@@ -1,21 +1,34 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Objects.Legacy
{
///
- /// A hit object only used for conversion, not actual gameplay.
+ /// Represents a legacy hit object.
///
- internal abstract class ConvertHitObject : HitObject, IHasCombo
+ ///
+ /// Only used for parsing beatmaps and not gameplay.
+ ///
+ internal abstract class ConvertHitObject : HitObject, IHasCombo, IHasPosition, IHasLegacyHitObjectType
{
public bool NewCombo { get; set; }
public int ComboOffset { get; set; }
+ public float X => Position.X;
+
+ public float Y => Position.Y;
+
+ public Vector2 Position { get; set; }
+
+ public LegacyHitObjectType LegacyType { get; set; }
+
public override Judgement CreateJudgement() => new IgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 89fd5f7384..f8bc0ce112 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osuTK;
using osu.Game.Rulesets.Objects.Types;
using System;
@@ -11,7 +9,6 @@ using System.IO;
using osu.Game.Beatmaps.Formats;
using osu.Game.Audio;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
@@ -24,24 +21,32 @@ namespace osu.Game.Rulesets.Objects.Legacy
///
/// A HitObjectParser to parse legacy Beatmaps.
///
- public abstract class ConvertHitObjectParser : HitObjectParser
+ public class ConvertHitObjectParser : HitObjectParser
{
///
/// The offset to apply to all time values.
///
- protected readonly double Offset;
+ private readonly double offset;
///
/// The .osu format (beatmap) version.
///
- protected readonly int FormatVersion;
+ private readonly int formatVersion;
- protected bool FirstObject { get; private set; } = true;
+ ///
+ /// Whether the current hitobject is the first hitobject in the beatmap.
+ ///
+ private bool firstObject = true;
- protected ConvertHitObjectParser(double offset, int formatVersion)
+ ///
+ /// The last parsed hitobject.
+ ///
+ private ConvertHitObject? lastObject;
+
+ internal ConvertHitObjectParser(double offset, int formatVersion)
{
- Offset = offset;
- FormatVersion = formatVersion;
+ this.offset = offset;
+ this.formatVersion = formatVersion;
}
public override HitObject Parse(string text)
@@ -49,11 +54,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
string[] split = text.Split(',');
Vector2 pos =
- FormatVersion >= LegacyBeatmapEncoder.FIRST_LAZER_VERSION
+ formatVersion >= LegacyBeatmapEncoder.FIRST_LAZER_VERSION
? new Vector2(Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE))
: new Vector2((int)Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE));
- double startTime = Parsing.ParseDouble(split[2]) + Offset;
+ double startTime = Parsing.ParseDouble(split[2]) + offset;
LegacyHitObjectType type = (LegacyHitObjectType)Parsing.ParseInt(split[3]);
@@ -66,11 +71,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]);
var bankInfo = new SampleBankInfo();
- HitObject result = null;
+ ConvertHitObject? result = null;
if (type.HasFlag(LegacyHitObjectType.Circle))
{
- result = CreateHit(pos, combo, comboOffset);
+ result = createHitCircle(pos, combo, comboOffset);
if (split.Length > 5)
readCustomSampleBanks(split[5], bankInfo);
@@ -145,13 +150,13 @@ namespace osu.Game.Rulesets.Objects.Legacy
for (int i = 0; i < nodes; i++)
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
- result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples);
+ result = createSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples);
}
else if (type.HasFlag(LegacyHitObjectType.Spinner))
{
- double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime);
+ double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + offset - startTime);
- result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, duration);
+ result = createSpinner(new Vector2(512, 384) / 2, combo, duration);
if (split.Length > 6)
readCustomSampleBanks(split[6], bankInfo);
@@ -169,18 +174,19 @@ namespace osu.Game.Rulesets.Objects.Legacy
readCustomSampleBanks(string.Join(':', ss.Skip(1)), bankInfo);
}
- result = CreateHold(pos, combo, comboOffset, endTime + Offset - startTime);
+ result = createHold(pos, endTime + offset - startTime);
}
if (result == null)
throw new InvalidDataException($"Unknown hit object type: {split[3]}");
result.StartTime = startTime;
+ result.LegacyType = type;
if (result.Samples.Count == 0)
result.Samples = convertSoundType(soundType, bankInfo);
- FirstObject = false;
+ firstObject = false;
return result;
}
@@ -200,10 +206,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (!Enum.IsDefined(addBank))
addBank = LegacySampleBank.Normal;
- string stringBank = bank.ToString().ToLowerInvariant();
+ string? stringBank = bank.ToString().ToLowerInvariant();
+ string? stringAddBank = addBank.ToString().ToLowerInvariant();
+
if (stringBank == @"none")
stringBank = null;
- string stringAddBank = addBank.ToString().ToLowerInvariant();
if (stringAddBank == @"none")
{
@@ -357,7 +364,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
{
int endPointLength = endPoint == null ? 0 : 1;
- if (FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
+ if (formatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
{
if (vertices.Length + endPointLength != 3)
type = PathType.BEZIER;
@@ -393,7 +400,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
// Legacy CATMULL sliders don't support multiple segments, so adjacent CATMULL segments should be treated as a single one.
// Importantly, this is not applied to the first control point, which may duplicate the slider path's position
// resulting in a duplicate (0,0) control point in the resultant list.
- if (type == PathType.CATMULL && endIndex > 1 && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
+ if (type == PathType.CATMULL && endIndex > 1 && formatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
continue;
// The last control point of each segment is not allowed to start a new implicit segment.
@@ -442,7 +449,15 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// Whether the hit object creates a new combo.
/// When starting a new combo, the offset of the new combo relative to the current one.
/// The hit object.
- protected abstract HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset);
+ private ConvertHitObject createHitCircle(Vector2 position, bool newCombo, int comboOffset)
+ {
+ return lastObject = new ConvertHitCircle
+ {
+ Position = position,
+ NewCombo = firstObject || lastObject is ConvertSpinner || newCombo,
+ ComboOffset = newCombo ? comboOffset : 0
+ };
+ }
///
/// Creats a legacy Slider-type hit object.
@@ -455,27 +470,51 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// The slider repeat count.
/// The samples to be played when the slider nodes are hit. This includes the head and tail of the slider.
/// The hit object.
- protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
- IList> nodeSamples);
+ private ConvertHitObject createSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
+ IList> nodeSamples)
+ {
+ return lastObject = new ConvertSlider
+ {
+ Position = position,
+ NewCombo = firstObject || lastObject is ConvertSpinner || newCombo,
+ ComboOffset = newCombo ? comboOffset : 0,
+ Path = new SliderPath(controlPoints, length),
+ NodeSamples = nodeSamples,
+ RepeatCount = repeatCount
+ };
+ }
///
/// Creates a legacy Spinner-type hit object.
///
/// The position of the hit object.
/// Whether the hit object creates a new combo.
- /// When starting a new combo, the offset of the new combo relative to the current one.
/// The spinner duration.
/// The hit object.
- protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration);
+ private ConvertHitObject createSpinner(Vector2 position, bool newCombo, double duration)
+ {
+ return lastObject = new ConvertSpinner
+ {
+ Position = position,
+ Duration = duration,
+ NewCombo = newCombo
+ // Spinners cannot have combo offset.
+ };
+ }
///
/// Creates a legacy Hold-type hit object.
///
/// The position of the hit object.
- /// Whether the hit object creates a new combo.
- /// When starting a new combo, the offset of the new combo relative to the current one.
/// The hold duration.
- protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration);
+ private ConvertHitObject createHold(Vector2 position, double duration)
+ {
+ return lastObject = new ConvertHold
+ {
+ Position = position,
+ Duration = duration
+ };
+ }
private List convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo)
{
@@ -511,21 +550,19 @@ namespace osu.Game.Rulesets.Objects.Legacy
///
/// An optional overriding filename which causes all bank/sample specifications to be ignored.
///
- public string Filename;
+ public string? Filename;
///
/// The bank identifier to use for the base ("hitnormal") sample.
/// Transferred to when appropriate.
///
- [CanBeNull]
- public string BankForNormal;
+ public string? BankForNormal;
///
/// The bank identifier to use for additions ("hitwhistle", "hitfinish", "hitclap").
/// Transferred to when appropriate.
///
- [CanBeNull]
- public string BankForAdditions;
+ public string? BankForAdditions;
///
/// Hit sample volume (0-100).
@@ -548,8 +585,6 @@ namespace osu.Game.Rulesets.Objects.Legacy
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
}
-#nullable enable
-
public class LegacyHitSampleInfo : HitSampleInfo, IEquatable
{
public readonly int CustomSampleBank;
@@ -577,13 +612,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
IsLayered = isLayered;
}
- public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default, Optional newEditorAutoBank = default)
+ public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default,
+ Optional newEditorAutoBank = default)
=> With(newName, newBank, newVolume, newEditorAutoBank);
- public virtual LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, Optional newEditorAutoBank = default,
- Optional newCustomSampleBank = default,
- Optional newIsLayered = default)
- => new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered));
+ public virtual LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default,
+ Optional newEditorAutoBank = default, Optional newCustomSampleBank = default, Optional newIsLayered = default)
+ => new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank), newCustomSampleBank.GetOr(CustomSampleBank),
+ newIsLayered.GetOr(IsLayered));
public bool Equals(LegacyHitSampleInfo? other)
// The additions to equality checks here are *required* to ensure that pooling works correctly.
@@ -615,9 +651,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
Path.ChangeExtension(Filename, null)
};
- public sealed override LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, Optional newEditorAutoBank = default,
- Optional newCustomSampleBank = default,
- Optional newIsLayered = default)
+ public sealed override LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default,
+ Optional newEditorAutoBank = default, Optional newCustomSampleBank = default, Optional newIsLayered = default)
=> new FileHitSampleInfo(Filename, newVolume.GetOr(Volume));
public bool Equals(FileHitSampleInfo? other)
diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHold.cs
similarity index 54%
rename from osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs
rename to osu.Game/Rulesets/Objects/Legacy/ConvertHold.cs
index 5ef3d51cb3..d74224892b 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHold.cs
@@ -3,17 +3,18 @@
using osu.Game.Rulesets.Objects.Types;
-namespace osu.Game.Rulesets.Objects.Legacy.Catch
+namespace osu.Game.Rulesets.Objects.Legacy
{
///
- /// Legacy osu!catch Spinner-type, used for parsing Beatmaps.
+ /// Legacy "Hold" hit object type. Generally only valid in the mania ruleset.
///
- internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition
+ ///
+ /// Only used for parsing beatmaps and not gameplay.
+ ///
+ internal sealed class ConvertHold : ConvertHitObject, IHasDuration
{
- public double EndTime => StartTime + Duration;
-
public double Duration { get; set; }
- public float X => 256; // Required for CatchBeatmapConverter
+ public double EndTime => StartTime + Duration;
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
index 683eefa8f4..fee68f2f11 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
using Newtonsoft.Json;
@@ -13,7 +11,13 @@ using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Objects.Legacy
{
- internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasSliderVelocity
+ ///
+ /// Legacy "Slider" hit object type.
+ ///
+ ///
+ /// Only used for parsing beatmaps and not gameplay.
+ ///
+ internal class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks
{
///
/// Scoring distance with a speed-adjusted beat length of 1 second.
@@ -50,6 +54,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
set => SliderVelocityMultiplierBindable.Value = value;
}
+ public bool GenerateTicks { get; set; } = true;
+
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSpinner.cs
similarity index 70%
rename from osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs
rename to osu.Game/Rulesets/Objects/Legacy/ConvertSpinner.cs
index 1d5ecb1ef3..59551cd37a 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSpinner.cs
@@ -3,11 +3,14 @@
using osu.Game.Rulesets.Objects.Types;
-namespace osu.Game.Rulesets.Objects.Legacy.Taiko
+namespace osu.Game.Rulesets.Objects.Legacy
{
///
- /// Legacy osu!taiko Spinner-type, used for parsing Beatmaps.
+ /// Legacy "Spinner" hit object type.
///
+ ///
+ /// Only used for parsing beatmaps and not gameplay.
+ ///
internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration
{
public double Duration { get; set; }
diff --git a/osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs b/osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs
new file mode 100644
index 0000000000..71af57700d
--- /dev/null
+++ b/osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs
@@ -0,0 +1,18 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps.Legacy;
+
+namespace osu.Game.Rulesets.Objects.Legacy
+{
+ ///
+ /// A hit object from a legacy beatmap representation.
+ ///
+ public interface IHasLegacyHitObjectType
+ {
+ ///
+ /// The hit object type.
+ ///
+ LegacyHitObjectType LegacyType { get; }
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs
deleted file mode 100644
index 0b69817c13..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Objects.Types;
-
-namespace osu.Game.Rulesets.Objects.Legacy.Mania
-{
- ///
- /// Legacy osu!mania Hit-type, used for parsing Beatmaps.
- ///
- internal sealed class ConvertHit : ConvertHitObject, IHasXPosition
- {
- public float X { get; set; }
- }
-}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs
deleted file mode 100644
index 386eb8d3ee..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osuTK;
-using osu.Game.Audio;
-using System.Collections.Generic;
-
-namespace osu.Game.Rulesets.Objects.Legacy.Mania
-{
- ///
- /// A HitObjectParser to parse legacy osu!mania Beatmaps.
- ///
- public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
- {
- public ConvertHitObjectParser(double offset, int formatVersion)
- : base(offset, formatVersion)
- {
- }
-
- protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
- {
- return new ConvertHit
- {
- X = position.X
- };
- }
-
- protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
- IList> nodeSamples)
- {
- return new ConvertSlider
- {
- X = position.X,
- Path = new SliderPath(controlPoints, length),
- NodeSamples = nodeSamples,
- RepeatCount = repeatCount
- };
- }
-
- protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
- {
- return new ConvertSpinner
- {
- X = position.X,
- Duration = duration
- };
- }
-
- protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
- {
- return new ConvertHold
- {
- X = position.X,
- Duration = duration
- };
- }
- }
-}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs
deleted file mode 100644
index 2fa4766c1d..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Objects.Types;
-
-namespace osu.Game.Rulesets.Objects.Legacy.Mania
-{
- internal sealed class ConvertHold : ConvertHitObject, IHasXPosition, IHasDuration
- {
- public float X { get; set; }
-
- public double Duration { get; set; }
-
- public double EndTime => StartTime + Duration;
- }
-}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs
deleted file mode 100644
index 84cde5fa95..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Objects.Types;
-
-namespace osu.Game.Rulesets.Objects.Legacy.Mania
-{
- ///
- /// Legacy osu!mania Slider-type, used for parsing Beatmaps.
- ///
- internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition
- {
- public float X { get; set; }
- }
-}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs
deleted file mode 100644
index c05aaceb9c..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Objects.Types;
-
-namespace osu.Game.Rulesets.Objects.Legacy.Mania
-{
- ///
- /// Legacy osu!mania Spinner-type, used for parsing Beatmaps.
- ///
- internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition
- {
- public double Duration { get; set; }
-
- public double EndTime => StartTime + Duration;
-
- public float X { get; set; }
- }
-}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs
deleted file mode 100644
index b7cd4b0dcc..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Objects.Types;
-using osuTK;
-
-namespace osu.Game.Rulesets.Objects.Legacy.Osu
-{
- ///
- /// Legacy osu! Hit-type, used for parsing Beatmaps.
- ///
- internal sealed class ConvertHit : ConvertHitObject, IHasPosition
- {
- public Vector2 Position { get; set; }
-
- public float X => Position.X;
-
- public float Y => Position.Y;
- }
-}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs
deleted file mode 100644
index 43c346b621..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using osuTK;
-using System.Collections.Generic;
-using osu.Game.Audio;
-
-namespace osu.Game.Rulesets.Objects.Legacy.Osu
-{
- ///
- /// A HitObjectParser to parse legacy osu! Beatmaps.
- ///
- public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
- {
- private ConvertHitObject lastObject;
-
- public ConvertHitObjectParser(double offset, int formatVersion)
- : base(offset, formatVersion)
- {
- }
-
- protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
- {
- return lastObject = new ConvertHit
- {
- Position = position,
- NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo,
- ComboOffset = newCombo ? comboOffset : 0
- };
- }
-
- protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
- IList> nodeSamples)
- {
- return lastObject = new ConvertSlider
- {
- Position = position,
- NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo,
- ComboOffset = newCombo ? comboOffset : 0,
- Path = new SliderPath(controlPoints, length),
- NodeSamples = nodeSamples,
- RepeatCount = repeatCount
- };
- }
-
- protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
- {
- return lastObject = new ConvertSpinner
- {
- Position = position,
- Duration = duration,
- NewCombo = newCombo
- // Spinners cannot have combo offset.
- };
- }
-
- protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
- {
- return lastObject = null;
- }
- }
-}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs
deleted file mode 100644
index 8c37154f95..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Objects.Types;
-using osuTK;
-
-namespace osu.Game.Rulesets.Objects.Legacy.Osu
-{
- ///
- /// Legacy osu! Slider-type, used for parsing Beatmaps.
- ///
- internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasGenerateTicks
- {
- public Vector2 Position { get; set; }
-
- public float X => Position.X;
-
- public float Y => Position.Y;
-
- public bool GenerateTicks { get; set; } = true;
- }
-}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs
deleted file mode 100644
index d6e24b6bbf..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Objects.Types;
-using osuTK;
-
-namespace osu.Game.Rulesets.Objects.Legacy.Osu
-{
- ///
- /// Legacy osu! Spinner-type, used for parsing Beatmaps.
- ///
- internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasPosition
- {
- public double Duration { get; set; }
-
- public double EndTime => StartTime + Duration;
-
- public Vector2 Position { get; set; }
-
- public float X => Position.X;
-
- public float Y => Position.Y;
- }
-}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs
deleted file mode 100644
index cb5178ce48..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-namespace osu.Game.Rulesets.Objects.Legacy.Taiko
-{
- ///
- /// Legacy osu!taiko Hit-type, used for parsing Beatmaps.
- ///
- internal sealed class ConvertHit : ConvertHitObject
- {
- }
-}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs
deleted file mode 100644
index d62e8cd04c..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using osuTK;
-using System.Collections.Generic;
-using osu.Game.Audio;
-
-namespace osu.Game.Rulesets.Objects.Legacy.Taiko
-{
- ///
- /// A HitObjectParser to parse legacy osu!taiko Beatmaps.
- ///
- public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
- {
- public ConvertHitObjectParser(double offset, int formatVersion)
- : base(offset, formatVersion)
- {
- }
-
- protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
- {
- return new ConvertHit();
- }
-
- protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
- IList> nodeSamples)
- {
- return new ConvertSlider
- {
- Path = new SliderPath(controlPoints, length),
- NodeSamples = nodeSamples,
- RepeatCount = repeatCount
- };
- }
-
- protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
- {
- return new ConvertSpinner
- {
- Duration = duration
- };
- }
-
- protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
- {
- return null;
- }
- }
-}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs
deleted file mode 100644
index 821554f7ee..0000000000
--- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-namespace osu.Game.Rulesets.Objects.Legacy.Taiko
-{
- ///
- /// Legacy osu!taiko Slider-type, used for parsing Beatmaps.
- ///
- internal sealed class ConvertSlider : Legacy.ConvertSlider
- {
- }
-}
diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
index 6e2852676a..fc4eef13ba 100644
--- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
+++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Scoring
foreach (var e in hitEvents)
{
- if (!affectsUnstableRate(e))
+ if (!AffectsUnstableRate(e))
continue;
count++;
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Scoring
///
public static double? CalculateAverageHitError(this IEnumerable hitEvents)
{
- double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).ToArray();
+ double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).ToArray();
if (timeOffsets.Length == 0)
return null;
@@ -65,6 +65,6 @@ namespace osu.Game.Rulesets.Scoring
return timeOffsets.Average();
}
- private static bool affectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit();
+ public static bool AffectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit();
}
}
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index 9752918dfb..7b5af9beda 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -119,6 +119,11 @@ namespace osu.Game.Rulesets.Scoring
///
public long MaximumTotalScore { get; private set; }
+ ///
+ /// The maximum achievable combo.
+ ///
+ public int MaximumCombo { get; private set; }
+
///
/// The maximum sum of accuracy-affecting judgements at the current point in time.
///
@@ -423,6 +428,7 @@ namespace osu.Game.Rulesets.Scoring
MaximumResultCounts.AddRange(ScoreResultCounts);
MaximumTotalScore = TotalScore.Value;
+ MaximumCombo = HighestCombo.Value;
}
ScoreResultCounts.Clear();
diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs
index ca774c34e7..a9dbfc29a9 100644
--- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs
@@ -258,6 +258,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void resetTernaryStates()
{
+ if (SelectionNewComboState.Value == TernaryState.Indeterminate)
+ SelectionNewComboState.Value = TernaryState.False;
AutoSelectionBankEnabled.Value = true;
SelectionAdditionBanksEnabled.Value = true;
SelectionBankStates[HIT_BANK_AUTO].Value = TernaryState.True;
@@ -269,7 +271,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
///
protected virtual void UpdateTernaryStates()
{
- SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType(), h => h.NewCombo);
+ if (SelectedItems.Any())
+ SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType(), h => h.NewCombo);
AutoSelectionBankEnabled.Value = SelectedItems.Count == 0;
var samplesInSelection = SelectedItems.SelectMany(enumerateAllSamples).ToArray();
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs
index 4b357d3a62..76323ac08c 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs
@@ -17,6 +17,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
protected OsuSpriteText Label { get; private set; }
+ protected Container LabelContainer { get; private set; }
+
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@@ -26,7 +28,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
InternalChildren = new Drawable[]
{
- new Container
+ LabelContainer = new Container
{
AutoSizeAxes = Axes.X,
Height = 16,
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
index 07833694c5..c3a56c8df9 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
@@ -16,6 +16,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
+using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
@@ -40,6 +41,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private Editor? editor { get; set; }
+ [Resolved]
+ private TimelineBlueprintContainer? timelineBlueprintContainer { get; set; }
+
public SamplePointPiece(HitObject hitObject)
{
HitObject = hitObject;
@@ -53,15 +57,45 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected virtual double GetTime() => HitObject is IHasRepeats r ? HitObject.StartTime + r.Duration / r.SpanCount() / 2 : HitObject.StartTime;
[BackgroundDependencyLoader]
- private void load()
+ private void load(OsuConfigManager config)
{
HitObject.DefaultsApplied += _ => updateText();
+ Label.AllowMultiline = false;
+ LabelContainer.AutoSizeAxes = Axes.None;
updateText();
if (editor != null)
editor.ShowSampleEditPopoverRequested += onShowSampleEditPopoverRequested;
}
+ private readonly Bindable contracted = new Bindable();
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (timelineBlueprintContainer != null)
+ contracted.BindTo(timelineBlueprintContainer.SamplePointContracted);
+
+ contracted.BindValueChanged(v =>
+ {
+ if (v.NewValue)
+ {
+ Label.FadeOut(200, Easing.OutQuint);
+ LabelContainer.ResizeTo(new Vector2(12), 200, Easing.OutQuint);
+ LabelContainer.CornerRadius = 6;
+ }
+ else
+ {
+ Label.FadeIn(200, Easing.OutQuint);
+ LabelContainer.ResizeTo(new Vector2(Label.Width, 16), 200, Easing.OutQuint);
+ LabelContainer.CornerRadius = 8;
+ }
+ }, true);
+
+ FinishTransforms();
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
@@ -87,6 +121,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateText()
{
Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}";
+
+ if (!contracted.Value)
+ LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint);
}
private static string? abbreviateBank(string? bank)
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
index a5d58215e8..a4083f58b6 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
@@ -19,17 +19,22 @@ using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
+ [Cached]
internal partial class TimelineBlueprintContainer : EditorBlueprintContainer
{
[Resolved(CanBeNull = true)]
private Timeline timeline { get; set; }
+ [Resolved(CanBeNull = true)]
+ private EditorClock editorClock { get; set; }
+
private Bindable placement;
private SelectionBlueprint placementBlueprint;
@@ -118,9 +123,53 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
base.Update();
+ updateSamplePointContractedState();
updateStacking();
}
+ public Bindable SamplePointContracted = new Bindable();
+
+ private void updateSamplePointContractedState()
+ {
+ const double minimum_gap = 28;
+
+ if (timeline == null || editorClock == null)
+ return;
+
+ // Find the smallest time gap between any two sample point pieces
+ double smallestTimeGap = double.PositiveInfinity;
+ double lastTime = double.PositiveInfinity;
+
+ // The blueprints are ordered in reverse chronological order
+ foreach (var selectionBlueprint in SelectionBlueprints)
+ {
+ var hitObject = selectionBlueprint.Item;
+
+ // Only check the hit objects which are visible in the timeline
+ // SelectionBlueprints can contain hit objects which are not visible in the timeline due to selection keeping them alive
+ if (hitObject.StartTime > editorClock.CurrentTime + timeline.VisibleRange / 2)
+ continue;
+
+ if (hitObject.GetEndTime() < editorClock.CurrentTime - timeline.VisibleRange / 2)
+ break;
+
+ if (hitObject is IHasRepeats hasRepeats)
+ smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2);
+
+ double gap = lastTime - hitObject.GetEndTime();
+
+ // If the gap is less than 1ms, we can assume that the objects are stacked on top of each other
+ // Contracting doesn't make sense in this case
+ if (gap > 1 && gap < smallestTimeGap)
+ smallestTimeGap = gap;
+
+ lastTime = hitObject.StartTime;
+ }
+
+ double smallestAbsoluteGap = ((TimelineSelectionBlueprintContainer)SelectionBlueprints).ContentRelativeToAbsoluteFactor.X * smallestTimeGap;
+ SamplePointContracted.Value = smallestAbsoluteGap < minimum_gap;
+ }
+
private readonly Stack currentConcurrentObjects = new Stack();
private void updateStacking()
@@ -291,6 +340,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
protected override HitObjectOrderedSelectionContainer Content { get; }
+ public Vector2 ContentRelativeToAbsoluteFactor => Content.RelativeToAbsoluteFactor;
+
public TimelineSelectionBlueprintContainer()
{
AddInternal(new TimelinePart>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
index 848c8f9a0f..31a0936eb4 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms;
@@ -32,10 +33,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override Container Content => zoomedContent;
///
- /// The current zoom level of .
+ /// The current (final) zoom level of .
/// It may differ from during transitions.
///
- public float CurrentZoom { get; private set; } = 1;
+ public BindableFloat CurrentZoom { get; private set; } = new BindableFloat(1);
private bool isZoomSetUp;
@@ -98,7 +99,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
minZoom = minimum;
maxZoom = maximum;
- CurrentZoom = zoomTarget = initial;
+ CurrentZoom.Value = zoomTarget = initial;
zoomedContentWidthCache.Invalidate();
isZoomSetUp = true;
@@ -124,7 +125,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (IsLoaded)
setZoomTarget(newZoom, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X);
else
- CurrentZoom = zoomTarget = newZoom;
+ CurrentZoom.Value = zoomTarget = newZoom;
}
protected override void UpdateAfterChildren()
@@ -154,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateZoomedContentWidth()
{
- zoomedContent.Width = DrawWidth * CurrentZoom;
+ zoomedContent.Width = DrawWidth * CurrentZoom.Value;
zoomedContentWidthCache.Validate();
}
@@ -238,7 +239,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
float expectedWidth = d.DrawWidth * newZoom;
float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset;
- d.CurrentZoom = newZoom;
+ d.CurrentZoom.Value = newZoom;
d.updateZoomedContentWidth();
// Temporarily here to make sure ScrollTo gets the correct DrawSize for scrollable area.
@@ -247,7 +248,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
d.ScrollTo(targetOffset, false);
}
- protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.CurrentZoom;
+ protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.CurrentZoom.Value;
}
}
}
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index 0997ab8003..41920605b0 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Screens.Menu
public Action? OnEditBeatmap;
public Action? OnEditSkin;
- public Action? OnExit;
+ public Action? OnExit;
public Action? OnBeatmapListing;
public Action? OnSolo;
public Action? OnSettings;
@@ -104,11 +104,11 @@ namespace osu.Game.Screens.Menu
buttonArea.AddRange(new Drawable[]
{
- new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), _ => OnSettings?.Invoke(), Key.O, Key.S)
+ new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), (_, _) => OnSettings?.Invoke(), Key.O, Key.S)
{
Padding = new MarginPadding { Right = WEDGE_WIDTH },
},
- backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), _ => State = ButtonSystemState.TopLevel)
+ backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), (_, _) => State = ButtonSystemState.TopLevel)
{
Padding = new MarginPadding { Right = WEDGE_WIDTH },
VisibleStateMin = ButtonSystemState.Play,
@@ -132,7 +132,7 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader]
private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host)
{
- buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => OnSolo?.Invoke(), Key.P)
+ buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), (_, _) => OnSolo?.Invoke(), Key.P)
{
Padding = new MarginPadding { Left = WEDGE_WIDTH },
});
@@ -141,22 +141,22 @@ namespace osu.Game.Screens.Menu
buttonsPlay.Add(new DailyChallengeButton(@"button-daily-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
- buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), _ => OnEditBeatmap?.Invoke(), Key.B, Key.E)
+ buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), (_, _) => OnEditBeatmap?.Invoke(), Key.B, Key.E)
{
Padding = new MarginPadding { Left = WEDGE_WIDTH },
});
- buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), _ => OnEditSkin?.Invoke(), Key.S));
+ buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), (_, _) => OnEditSkin?.Invoke(), Key.S));
buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit);
- buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), _ => State = ButtonSystemState.Play, Key.P, Key.M, Key.L)
+ buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), (_, _) => State = ButtonSystemState.Play, Key.P, Key.M, Key.L)
{
Padding = new MarginPadding { Left = WEDGE_WIDTH },
});
- buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), _ => State = ButtonSystemState.Edit, Key.E));
- buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), _ => OnBeatmapListing?.Invoke(), Key.B, Key.D));
+ buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), (_, _) => State = ButtonSystemState.Edit, Key.E));
+ buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), (_, _) => OnBeatmapListing?.Invoke(), Key.B, Key.D));
if (host.CanExit)
- buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), _ => OnExit?.Invoke(), Key.Q));
+ buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), (_, e) => OnExit?.Invoke(e), Key.Q));
buttonArea.AddRange(buttonsPlay);
buttonArea.AddRange(buttonsEdit);
@@ -179,7 +179,7 @@ namespace osu.Game.Screens.Menu
sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh");
}
- private void onMultiplayer(MainMenuButton _)
+ private void onMultiplayer(MainMenuButton mainMenuButton, UIEvent uiEvent)
{
if (api.State.Value != APIState.Online)
{
@@ -190,7 +190,7 @@ namespace osu.Game.Screens.Menu
OnMultiplayer?.Invoke();
}
- private void onPlaylists(MainMenuButton _)
+ private void onPlaylists(MainMenuButton mainMenuButton, UIEvent uiEvent)
{
if (api.State.Value != APIState.Online)
{
@@ -201,7 +201,7 @@ namespace osu.Game.Screens.Menu
OnPlaylists?.Invoke();
}
- private void onDailyChallenge(MainMenuButton button)
+ private void onDailyChallenge(MainMenuButton button, UIEvent uiEvent)
{
if (api.State.Value != APIState.Online)
{
diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs
index 4dbebf0ae9..44a53efa7b 100644
--- a/osu.Game/Screens/Menu/DailyChallengeButton.cs
+++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps.Drawables;
@@ -50,7 +51,7 @@ namespace osu.Game.Screens.Menu
[Resolved]
private SessionStatics statics { get; set; } = null!;
- public DailyChallengeButton(string sampleName, Color4 colour, Action? clickAction = null, params Key[] triggerKeys)
+ public DailyChallengeButton(string sampleName, Color4 colour, Action? clickAction = null, params Key[] triggerKeys)
: base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys)
{
BaseSize = new Vector2(ButtonSystem.BUTTON_WIDTH * 1.3f, ButtonArea.BUTTON_AREA_HEIGHT);
diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs
index 346bdcb751..35c6bab81b 100644
--- a/osu.Game/Screens/Menu/MainMenu.cs
+++ b/osu.Game/Screens/Menu/MainMenu.cs
@@ -153,9 +153,9 @@ namespace osu.Game.Screens.Menu
else
this.Push(new DailyChallengeIntro(room));
},
- OnExit = () =>
+ OnExit = e =>
{
- exitConfirmedViaHoldOrClick = true;
+ exitConfirmedViaHoldOrClick = e is MouseEvent;
this.Exit();
}
}
diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs
index 4df5e6d309..f8824795d8 100644
--- a/osu.Game/Screens/Menu/MainMenuButton.cs
+++ b/osu.Game/Screens/Menu/MainMenuButton.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Screens.Menu
protected Vector2 BaseSize { get; init; } = new Vector2(ButtonSystem.BUTTON_WIDTH, ButtonArea.BUTTON_AREA_HEIGHT);
- private readonly Action? clickAction;
+ private readonly Action? clickAction;
private readonly Container background;
private readonly Drawable backgroundContent;
@@ -84,7 +84,7 @@ namespace osu.Game.Screens.Menu
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => background.ReceivePositionalInputAt(screenSpacePos);
- public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action? clickAction = null, params Key[] triggerKeys)
+ public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action? clickAction = null, params Key[] triggerKeys)
{
this.sampleName = sampleName;
this.clickAction = clickAction;
@@ -263,7 +263,7 @@ namespace osu.Game.Screens.Menu
protected override bool OnClick(ClickEvent e)
{
- trigger();
+ trigger(e);
return true;
}
@@ -274,19 +274,19 @@ namespace osu.Game.Screens.Menu
if (TriggerKeys.Contains(e.Key))
{
- trigger();
+ trigger(e);
return true;
}
return false;
}
- private void trigger()
+ private void trigger(UIEvent e)
{
sampleChannel = sampleClick?.GetChannel();
sampleChannel?.Play();
- clickAction?.Invoke(this);
+ clickAction?.Invoke(this, e);
boxHoverLayer.ClearTransforms();
boxHoverLayer.Alpha = 0.9f;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index e560c5ca5d..a50f3440f5 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -158,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!string.IsNullOrEmpty(message))
Logger.Log(message, LoggingTarget.Runtime, LogLevel.Important);
- Schedule(() => PerformExit(false));
+ Schedule(() => PerformExit());
}
private void onGameplayStarted() => Scheduler.Add(() =>
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 398e8df5c9..e9722350bd 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -86,6 +86,7 @@ namespace osu.Game.Screens.Play
public Action RestartRequested;
private bool isRestarting;
+ private bool skipExitTransition;
private Bindable mouseWheelDisabled;
@@ -289,7 +290,7 @@ namespace osu.Game.Screens.Play
{
SaveReplay = async () => await prepareAndImportScoreAsync(true).ConfigureAwait(false),
OnRetry = Configuration.AllowUserInteraction ? () => Restart() : null,
- OnQuit = () => PerformExit(true),
+ OnQuit = () => PerformExitWithConfirmation(),
},
new HotkeyExitOverlay
{
@@ -297,10 +298,7 @@ namespace osu.Game.Screens.Play
{
if (!this.IsCurrentScreen()) return;
- if (PerformExit(false))
- // The hotkey overlay dims the screen.
- // If the operation succeeds, we want to make sure we stay dimmed to keep continuity.
- fadeOut(true);
+ PerformExit(skipTransition: true);
},
},
});
@@ -318,10 +316,7 @@ namespace osu.Game.Screens.Play
{
if (!this.IsCurrentScreen()) return;
- if (Restart(true))
- // The hotkey overlay dims the screen.
- // If the operation succeeds, we want to make sure we stay dimmed to keep continuity.
- fadeOut(true);
+ Restart(true);
},
},
});
@@ -448,7 +443,7 @@ namespace osu.Game.Screens.Play
{
HoldToQuit =
{
- Action = () => PerformExit(true),
+ Action = () => PerformExitWithConfirmation(),
IsPaused = { BindTarget = GameplayClockContainer.IsPaused },
ReplayLoaded = { BindTarget = DrawableRuleset.HasReplayLoaded },
},
@@ -485,7 +480,7 @@ namespace osu.Game.Screens.Play
OnResume = Resume,
Retries = RestartCount,
OnRetry = () => Restart(),
- OnQuit = () => PerformExit(true),
+ OnQuit = () => PerformExitWithConfirmation(),
},
},
};
@@ -588,25 +583,24 @@ namespace osu.Game.Screens.Play
}
///
- /// Attempts to complete a user request to exit gameplay.
+ /// Attempts to complete a user request to exit gameplay, with confirmation.
///
///
///
/// - This should only be called in response to a user interaction. Exiting is not guaranteed.
/// - This will interrupt any pending progression to the results screen, even if the transition has begun.
///
+ ///
+ /// This method will show the pause or fail dialog before performing an exit.
+ /// If a dialog is not yet displayed, the exit will be blocked and the relevant dialog will display instead.
///
- ///
- /// Whether the pause or fail dialog should be shown before performing an exit.
- /// If and a dialog is not yet displayed, the exit will be blocked and the relevant dialog will display instead.
- ///
/// Whether this call resulted in a final exit.
- protected bool PerformExit(bool showDialogFirst)
+ protected bool PerformExitWithConfirmation()
{
bool pauseOrFailDialogVisible =
PauseOverlay.State.Value == Visibility.Visible || FailOverlay.State.Value == Visibility.Visible;
- if (showDialogFirst && !pauseOrFailDialogVisible)
+ if (!pauseOrFailDialogVisible)
{
// if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion).
if (ValidForResume && GameplayState.HasFailed)
@@ -625,6 +619,22 @@ namespace osu.Game.Screens.Play
}
}
+ return PerformExit();
+ }
+
+ ///
+ /// Attempts to complete a user request to exit gameplay.
+ ///
+ ///
+ ///
+ /// - This should only be called in response to a user interaction. Exiting is not guaranteed.
+ /// - This will interrupt any pending progression to the results screen, even if the transition has begun.
+ ///
+ ///
+ /// Whether the exit should perform without a transition, because the screen had faded to black already.
+ /// Whether this call resulted in a final exit.
+ protected bool PerformExit(bool skipTransition = false)
+ {
// Matching osu!stable behaviour, if the results screen is pending and the user requests an exit,
// show the results instead.
if (GameplayState.HasPassed && !isRestarting)
@@ -639,6 +649,8 @@ namespace osu.Game.Screens.Play
// Screen may not be current if a restart has been performed.
if (this.IsCurrentScreen())
{
+ skipExitTransition = skipTransition;
+
// The actual exit is performed if
// - the pause / fail dialog was not requested
// - the pause / fail dialog was requested but is already displayed (user showing intention to exit).
@@ -707,9 +719,14 @@ namespace osu.Game.Screens.Play
// stopping here is to ensure music doesn't become audible after exiting back to PlayerLoader.
musicController.Stop();
- RestartRequested?.Invoke(quickRestart);
+ if (RestartRequested != null)
+ {
+ skipExitTransition = quickRestart;
+ RestartRequested?.Invoke(quickRestart);
+ return true;
+ }
- return PerformExit(false);
+ return PerformExit(quickRestart);
}
///
@@ -1254,10 +1271,10 @@ namespace osu.Game.Screens.Play
ShowUserStatistics = true,
};
- private void fadeOut(bool instant = false)
+ private void fadeOut()
{
- float fadeOutDuration = instant ? 0 : 250;
- this.FadeOut(fadeOutDuration);
+ if (!skipExitTransition)
+ this.FadeOut(250);
if (this.IsCurrentScreen())
{
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 9b4bf7d633..aedc268d70 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -10,6 +10,7 @@ using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Input;
@@ -51,6 +52,8 @@ namespace osu.Game.Screens.Play
public override bool? AllowGlobalTrackControl => false;
+ public override float BackgroundParallaxAmount => quickRestart ? 0 : 1;
+
// Here because IsHovered will not update unless we do so.
public override bool HandlePositionalInput => true;
@@ -86,9 +89,13 @@ namespace osu.Game.Screens.Play
private SkinnableSound sampleRestart = null!;
+ private Box? quickRestartBlackLayer;
+
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+ private const double quick_restart_initial_delay = 500;
+
protected bool BackgroundBrightnessReduction
{
set
@@ -224,7 +231,7 @@ namespace osu.Game.Screens.Play
}
},
},
- idleTracker = new IdleTracker(750),
+ idleTracker = new IdleTracker(1500),
sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click"))
};
@@ -305,6 +312,9 @@ namespace osu.Game.Screens.Play
{
base.OnSuspending(e);
+ quickRestartBlackLayer?.FadeOut(500, Easing.OutQuint).Expire();
+ quickRestartBlackLayer = null;
+
BackgroundBrightnessReduction = false;
// we're moving to player, so a period of silence is upcoming.
@@ -321,6 +331,9 @@ namespace osu.Game.Screens.Play
cancelLoad();
ContentOut();
+ quickRestartBlackLayer?.FadeOut(100, Easing.OutQuint).Expire();
+ quickRestartBlackLayer = null;
+
// Ensure the screen doesn't expire until all the outwards fade operations have completed.
this.Delay(CONTENT_OUT_DURATION).FadeOut();
@@ -348,7 +361,14 @@ namespace osu.Game.Screens.Play
if (!resuming) logo.MoveTo(new Vector2(0.5f), duration, Easing.OutQuint);
logo.ScaleTo(new Vector2(0.15f), duration, Easing.OutQuint);
- logo.FadeIn(350);
+
+ if (quickRestart)
+ {
+ logo.Delay(quick_restart_initial_delay)
+ .FadeIn(350);
+ }
+ else
+ logo.FadeIn(350);
Scheduler.AddDelayed(() =>
{
@@ -387,7 +407,7 @@ namespace osu.Game.Screens.Play
// We need to perform this check here rather than in OnHover as any number of children of VisualSettings
// may also be handling the hover events.
- if (inputManager.HoveredDrawables.Contains(VisualSettings))
+ if (inputManager.HoveredDrawables.Contains(VisualSettings) || quickRestart)
{
// Preview user-defined background dim and blur when hovered on the visual settings panel.
ApplyToBackground(b =>
@@ -454,28 +474,45 @@ namespace osu.Game.Screens.Play
{
MetadataInfo.Loading = true;
- content.FadeInFromZero(500, Easing.OutQuint);
-
if (quickRestart)
{
+ // A quick restart starts by triggering a fade to black
+ AddInternal(quickRestartBlackLayer = new Box
+ {
+ Colour = Color4.Black,
+ RelativeSizeAxes = Axes.Both,
+ Depth = float.MaxValue
+ });
+
+ quickRestartBlackLayer
+ .Delay(50)
+ .FadeOut(5000, Easing.OutQuint);
+
prepareNewPlayer();
- content.ScaleTo(1, 650, Easing.OutQuint);
+
+ content
+ .Delay(quick_restart_initial_delay)
+ .ScaleTo(1)
+ .FadeInFromZero(500, Easing.OutQuint);
}
else
{
+ content.FadeInFromZero(500, Easing.OutQuint);
+
content
+ .ScaleTo(0.7f)
.ScaleTo(1, 650, Easing.OutQuint)
.Then()
.Schedule(prepareNewPlayer);
- }
- using (BeginDelayedSequence(delayBeforeSideDisplays))
- {
- settingsScroll.FadeInFromZero(500, Easing.Out)
- .MoveToX(0, 500, Easing.OutQuint);
+ using (BeginDelayedSequence(delayBeforeSideDisplays))
+ {
+ settingsScroll.FadeInFromZero(500, Easing.Out)
+ .MoveToX(0, 500, Easing.OutQuint);
- disclaimers.FadeInFromZero(500, Easing.Out)
- .MoveToX(0, 500, Easing.OutQuint);
+ disclaimers.FadeInFromZero(500, Easing.Out)
+ .MoveToX(0, 500, Easing.OutQuint);
+ }
}
AddRangeInternal(new[]
@@ -539,33 +576,36 @@ namespace osu.Game.Screens.Play
highPerformanceSession ??= highPerformanceSessionManager?.BeginSession();
scheduledPushPlayer = Scheduler.AddDelayed(() =>
- {
- // ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared).
- var consumedPlayer = consumePlayer();
-
- ContentOut();
-
- TransformSequence pushSequence = this.Delay(0);
-
- // This goes hand-in-hand with the restoration of low pass filter in contentOut().
- this.TransformBindableTo(volumeAdjustment, 0, CONTENT_OUT_DURATION, Easing.OutCubic);
-
- pushSequence.Schedule(() =>
{
- if (!this.IsCurrentScreen()) return;
+ // ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared).
+ var consumedPlayer = consumePlayer();
- LoadTask = null;
+ ContentOut();
- // By default, we want to load the player and never be returned to.
- // Note that this may change if the player we load requested a re-run.
- ValidForResume = false;
+ TransformSequence pushSequence = this.Delay(0);
- if (consumedPlayer.LoadedBeatmapSuccessfully)
- this.Push(consumedPlayer);
- else
- this.Exit();
- });
- }, 500);
+ // This goes hand-in-hand with the restoration of low pass filter in contentOut().
+ this.TransformBindableTo(volumeAdjustment, 0, CONTENT_OUT_DURATION, Easing.OutCubic);
+
+ pushSequence.Schedule(() =>
+ {
+ if (!this.IsCurrentScreen()) return;
+
+ LoadTask = null;
+
+ // By default, we want to load the player and never be returned to.
+ // Note that this may change if the player we load requested a re-run.
+ ValidForResume = false;
+
+ if (consumedPlayer.LoadedBeatmapSuccessfully)
+ this.Push(consumedPlayer);
+ else
+ this.Exit();
+ });
+ },
+ // When a quick restart is activated, the metadata content will display some time later if it's taking too long.
+ // To avoid it appearing too briefly, if it begins to fade in let's induce a standard delay.
+ quickRestart && content.Alpha == 0 ? 0 : 500);
}
private void cancelLoad()
diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
index f312fb0ec5..74b887481f 100644
--- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
@@ -195,7 +195,10 @@ namespace osu.Game.Screens.Play.PlayerSettings
},
};
- if (hitEvents.Count < 10)
+ // affecting unstable rate here is used as a substitute of determining if a hit event represents a *timed* hit event,
+ // i.e. an user input that the user had to *time to the track*,
+ // i.e. one that it *makes sense to use* when doing anything with timing and offsets.
+ if (hitEvents.Count(HitEventExtensions.AffectsUnstableRate) < 10)
{
referenceScoreContainer.AddRange(new Drawable[]
{
diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs
index 7ea3cbe917..7d155e32b0 100644
--- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs
@@ -53,10 +53,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator();
// Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
- if (attributes?.Attributes == null || performanceCalculator == null)
+ if (attributes?.DifficultyAttributes == null || performanceCalculator == null)
return;
- var result = await performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken ?? default).ConfigureAwait(false);
+ var result = await performanceCalculator.CalculateAsync(score, attributes.Value.DifficultyAttributes, cancellationToken ?? default).ConfigureAwait(false);
Schedule(() => setPerformanceValue(score, result.Total));
}, cancellationToken ?? default);
diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs
index 207e19a716..a9b93e0ffc 100644
--- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs
+++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs
@@ -176,7 +176,7 @@ namespace osu.Game.Screens.Ranking.Statistics
for (int i = 1; i <= axis_points; i++)
{
double axisValue = i * axisValueStep;
- float position = (float)(axisValue / maxValue);
+ float position = maxValue == 0 ? 0 : (float)(axisValue / maxValue);
float alpha = 1f - position * 0.8f;
axisFlow.Add(new OsuSpriteText
@@ -348,7 +348,7 @@ namespace osu.Game.Screens.Ranking.Statistics
boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint);
}
- private float offsetForValue(float value) => (1 - minimum_height) * value / maxValue;
+ private float offsetForValue(float value) => maxValue == 0 ? 0 : (1 - minimum_height) * value / maxValue;
private float heightForValue(float value) => minimum_height + offsetForValue(value);
}
diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs
index b5eed2d12a..f9c8c93dec 100644
--- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs
+++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
+using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
@@ -26,7 +27,6 @@ namespace osu.Game.Screens.Ranking.Statistics
public partial class PerformanceBreakdownChart : Container
{
private readonly ScoreInfo score;
- private readonly IBeatmap playableBeatmap;
private Drawable spinner = null!;
private Drawable content = null!;
@@ -42,7 +42,6 @@ namespace osu.Game.Screens.Ranking.Statistics
public PerformanceBreakdownChart(ScoreInfo score, IBeatmap playableBeatmap)
{
this.score = score;
- this.playableBeatmap = playableBeatmap;
}
[BackgroundDependencyLoader]
@@ -142,12 +141,33 @@ namespace osu.Game.Screens.Ranking.Statistics
spinner.Show();
- new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache)
- .CalculateAsync(score, cancellationTokenSource.Token)
- .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()!)));
+ computePerformance(cancellationTokenSource.Token)
+ .ContinueWith(t => Schedule(() =>
+ {
+ if (t.GetResultSafely() is PerformanceBreakdown breakdown)
+ setPerformance(breakdown);
+ }), TaskContinuationOptions.OnlyOnRanToCompletion);
}
- private void setPerformanceValue(PerformanceBreakdown breakdown)
+ private async Task computePerformance(CancellationToken token)
+ {
+ var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator();
+ if (performanceCalculator == null)
+ return null;
+
+ var starsTask = difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, token).ConfigureAwait(false);
+ if (await starsTask is not StarDifficulty stars)
+ return null;
+
+ if (stars.DifficultyAttributes == null || stars.PerformanceAttributes == null)
+ return null;
+
+ return new PerformanceBreakdown(
+ await performanceCalculator.CalculateAsync(score, stars.DifficultyAttributes, token).ConfigureAwait(false),
+ stars.PerformanceAttributes);
+ }
+
+ private void setPerformance(PerformanceBreakdown breakdown)
{
spinner.Hide();
content.FadeIn(200);
@@ -236,6 +256,8 @@ namespace osu.Game.Screens.Ranking.Statistics
protected override void Dispose(bool isDisposing)
{
cancellationTokenSource.Cancel();
+ cancellationTokenSource.Dispose();
+
base.Dispose(isDisposing);
}
}
diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs
index 4e35c90ba6..79a1ed4d7c 100644
--- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs
+++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs
@@ -177,6 +177,9 @@ namespace osu.Game.Skinning.Components
case BeatmapAttribute.BPM:
return BeatmapsetsStrings.ShowStatsBpm;
+ case BeatmapAttribute.MaxPP:
+ return BeatmapAttributeTextStrings.MaxPP;
+
default:
return string.Empty;
}
@@ -225,6 +228,9 @@ namespace osu.Game.Skinning.Components
case BeatmapAttribute.StarRating:
return (starDifficulty?.Stars ?? 0).ToLocalisableString(@"F2");
+ case BeatmapAttribute.MaxPP:
+ return Math.Round(starDifficulty?.PerformanceAttributes?.Total ?? 0, MidpointRounding.AwayFromZero).ToLocalisableString();
+
default:
return string.Empty;
}
@@ -279,5 +285,6 @@ namespace osu.Game.Skinning.Components
RankedStatus,
BPM,
Source,
+ MaxPP
}
}
diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs
index a761ddaea7..5804fce4c1 100644
--- a/osu.Game/Users/UserRankPanel.cs
+++ b/osu.Game/Users/UserRankPanel.cs
@@ -163,7 +163,7 @@ namespace osu.Game.Users
Margin = new MarginPadding { Top = main_content_height },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Left = 80, Vertical = padding },
+ Padding = new MarginPadding(padding),
ColumnDimensions = new[]
{
new Dimension(),
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 855ba3374b..71e6fee78e 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -35,7 +35,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index ecb9ae2c8d..d4ef9a263f 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -17,6 +17,6 @@
-all
-
+