mirror of
https://github.com/ppy/osu.git
synced 2025-01-07 21:32:57 +08:00
Merge branch 'master' into user-statistics-provider
This commit is contained in:
commit
6c8a900dcc
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1025.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1115.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -26,7 +26,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="System.IO.Packaging" Version="8.0.1" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageReference Include="Velopack" Version="0.0.869" />
|
||||
<PackageReference Include="Velopack" Version="0.0.915" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
AddStep("start drag", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<Column>().First());
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<NoteSelectionBlueprint>().Single(blueprint => blueprint.IsSelected && blueprint.HitObject.StartTime == 0));
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
AddStep("end drag", () =>
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@ -73,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
// 0.0 +--------+-+---------------> 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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<HoldNote>
|
||||
{
|
||||
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
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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<HoldNote>
|
||||
{
|
||||
[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<ScrollingDirection> 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;
|
||||
|
@ -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<ScrollingDirection> 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<ScrollingDirection> direction);
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
|
@ -1,10 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.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<Note>
|
||||
{
|
||||
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)
|
||||
|
@ -1,18 +1,42 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.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<Note>
|
||||
{
|
||||
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<ScrollingDirection> direction)
|
||||
{
|
||||
notePiece.Scale = new Vector2(1, direction.NewValue == ScrollingDirection.Down ? 1 : -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
},
|
||||
columnBackgrounds = new ColumnFlow<Drawable>(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),
|
||||
|
@ -28,6 +28,12 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
private readonly FillFlowContainer<Container<TContent>> columns;
|
||||
private readonly StageDefinition stageDefinition;
|
||||
|
||||
public new bool Masking
|
||||
{
|
||||
get => base.Masking;
|
||||
set => base.Masking = value;
|
||||
}
|
||||
|
||||
public ColumnFlow(StageDefinition stageDefinition)
|
||||
{
|
||||
this.stageDefinition = stageDefinition;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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<Island>
|
||||
{
|
||||
private readonly double deltaDifferenceEpsilon;
|
||||
|
@ -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;
|
||||
|
@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
/// </summary>
|
||||
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;
|
||||
|
@ -23,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
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;
|
||||
|
@ -50,9 +50,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
[Resolved]
|
||||
private HitObjectComposer composer { get; set; } = null!;
|
||||
|
||||
private Bindable<TernaryState> 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<HitCircle>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
85
osu.Game.Rulesets.Osu/Mods/OsuModBloom.cs
Normal file
85
osu.Game.Rulesets.Osu/Mods/OsuModBloom.cs
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.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<int> CurrentCombo = new BindableInt();
|
||||
protected readonly IBindable<bool> IsBreakTime = new Bindable<bool>();
|
||||
|
||||
private float currentSize;
|
||||
|
||||
[SettingSource(
|
||||
"Max size at combo",
|
||||
"The combo count at which the cursor reaches its maximum size",
|
||||
SettingControlType = typeof(SettingsSlider<int, RoundedSliderBar<int>>)
|
||||
)]
|
||||
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<float, RoundedSliderBar<float>>)
|
||||
)]
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public partial class OsuModFlashlight : ModFlashlight<OsuHitObject>, 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;
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -214,7 +214,8 @@ namespace osu.Game.Rulesets.Osu
|
||||
new OsuModFreezeFrame(),
|
||||
new OsuModBubbles(),
|
||||
new OsuModSynesthesia(),
|
||||
new OsuModDepth()
|
||||
new OsuModDepth(),
|
||||
new OsuModBloom()
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
|
@ -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;
|
||||
|
@ -38,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
public IBindable<float> CursorScale => cursorScale;
|
||||
|
||||
/// <summary>
|
||||
/// Mods which want to adjust cursor size should do so via this bindable.
|
||||
/// </summary>
|
||||
public readonly Bindable<float> ModScaleAdjust = new Bindable<float>(1);
|
||||
|
||||
private readonly Bindable<float> cursorScale = new BindableFloat(1);
|
||||
|
||||
private Bindable<float> userCursorScale = null!;
|
||||
@ -67,6 +72,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
autoCursorScale = config.GetBindable<bool>(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)
|
||||
{
|
||||
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A sigmoid function. It gives a value between (middle - height/2) and (middle + height/2).
|
||||
/// </summary>
|
||||
/// <param name="val">The input value.</param>
|
||||
/// <param name="center">The center of the sigmoid, where the largest gradient occurs and value is equal to middle.</param>
|
||||
/// <param name="width">The radius of the sigmoid, outside of which values are near the minimum/maximum.</param>
|
||||
/// <param name="middle">The middle of the sigmoid output.</param>
|
||||
/// <param name="height">The height of the sigmoid output. This will be equal to max value - min value.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate the difficulty of the first note of a <see cref="MonoStreak"/>.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -38,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -46,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
/// </summary>
|
||||
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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
63
osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs
Normal file
63
osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.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]));
|
||||
}
|
||||
}
|
||||
}
|
BIN
osu.Game.Tests/Resources/Archives/decimal-timing-beatmap.olz
Normal file
BIN
osu.Game.Tests/Resources/Archives/decimal-timing-beatmap.olz
Normal file
Binary file not shown.
@ -205,7 +205,9 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
|
||||
AddStep("click first delete button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
|
||||
InputManager.MoveMouseTo(dialog
|
||||
.ChildrenOfType<DrawableCollectionListItem>().Single(i => i.Model.Value.Name == "1")
|
||||
.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().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<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
|
||||
InputManager.MoveMouseTo(dialog
|
||||
.ChildrenOfType<DrawableCollectionListItem>().Single(i => i.Model.Value.Name == "2")
|
||||
.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().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<DrawableCollectionListItem>().First());
|
||||
InputManager.MoveMouseTo(firstItem = dialog.ChildrenOfType<DrawableCollectionListItem>().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<DrawableCollectionListItem>().Count() == count + 1); // +1 for placeholder
|
||||
|
||||
private void assertCollectionName(int index, string name)
|
||||
=> AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType<DrawableCollectionListItem>().ElementAt(index).ChildrenOfType<TextBox>().First().Text == name);
|
||||
=> AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType<DrawableCollectionList>().Single().OrderedItems.ElementAt(index).ChildrenOfType<TextBox>().First().Text == name);
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -1,14 +1,18 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.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<SettingsButton>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNotEnoughTimedHitEvents()
|
||||
{
|
||||
AddStep("Set short reference score", () =>
|
||||
{
|
||||
List<HitEvent> 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<SettingsButton>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScoreFromDifferentBeatmap()
|
||||
{
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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<HitEvent>());
|
||||
AddStep("update offset", () => graph.UpdateOffset(10));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManyDistributedEventsOffset()
|
||||
{
|
||||
|
@ -1,12 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#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<IBeatmap> beatmaps = new List<IBeatmap>();
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
this.rulesets = rulesets;
|
||||
}
|
||||
private TestBeatmapInfoWedge infoWedge = null!;
|
||||
private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
|
||||
|
||||
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<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength));
|
||||
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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<SpriteText>().Single().Text.ToString();
|
||||
|
||||
private class TestRuleset : Ruleset
|
||||
|
@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
break;
|
||||
|
||||
case Key.Q:
|
||||
buttons.OnExit = action;
|
||||
buttons.OnExit = _ => action();
|
||||
break;
|
||||
|
||||
case Key.O:
|
||||
|
@ -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,
|
||||
|
@ -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<Mod>? mods)
|
||||
@ -317,5 +350,42 @@ namespace osu.Game.Beatmaps
|
||||
CancellationToken = cancellationToken;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private class PlayableCachedWorkingBeatmap : IWorkingBeatmap
|
||||
{
|
||||
private readonly IWorkingBeatmap working;
|
||||
private IBeatmap? playable;
|
||||
|
||||
public PlayableCachedWorkingBeatmap(IWorkingBeatmap working)
|
||||
{
|
||||
this.working = working;
|
||||
}
|
||||
|
||||
public IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<Mod> mods)
|
||||
=> playable ??= working.GetPlayableBeatmap(ruleset, mods);
|
||||
|
||||
public IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<Mod> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -6,7 +6,7 @@ using System;
|
||||
namespace osu.Game.Beatmaps.Legacy
|
||||
{
|
||||
[Flags]
|
||||
internal enum LegacyHitObjectType
|
||||
public enum LegacyHitObjectType
|
||||
{
|
||||
Circle = 1,
|
||||
Slider = 1 << 1,
|
||||
|
@ -1,9 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#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.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public readonly DifficultyAttributes Attributes;
|
||||
public readonly DifficultyAttributes? DifficultyAttributes;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="StarDifficulty"/> structure based on <see cref="DifficultyAttributes"/> computed
|
||||
/// by a <see cref="DifficultyCalculator"/>.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public StarDifficulty([NotNull] DifficultyAttributes attributes)
|
||||
public readonly PerformanceAttributes? PerformanceAttributes;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="StarDifficulty"/> structure.
|
||||
/// </summary>
|
||||
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...)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="StarDifficulty"/> structure with a pre-populated star difficulty and max combo
|
||||
/// in scenarios where computing <see cref="DifficultyAttributes"/> is not feasible (i.e. when working with online sources).
|
||||
/// in scenarios where computing <see cref="Rulesets.Difficulty.DifficultyAttributes"/> is not feasible (i.e. when working with online sources).
|
||||
/// </summary>
|
||||
public StarDifficulty(double starDifficulty, int maxCombo)
|
||||
{
|
||||
Stars = double.IsFinite(starDifficulty) ? starDifficulty : 0;
|
||||
MaxCombo = maxCombo;
|
||||
Attributes = null;
|
||||
}
|
||||
|
||||
public DifficultyRating DifficultyRating => GetDifficultyRating(Stars);
|
||||
|
@ -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<RearrangeableListItem<Live<BeatmapCollection>>> CreateListFillFlowContainer() => new Flow
|
||||
private Flow flow = null!;
|
||||
|
||||
public IEnumerable<Drawable> OrderedItems => flow.FlowingChildren;
|
||||
|
||||
protected override FillFlowContainer<RearrangeableListItem<Live<BeatmapCollection>>> CreateListFillFlowContainer() => flow = new Flow
|
||||
{
|
||||
DragActive = { BindTarget = DragActive }
|
||||
};
|
||||
@ -43,8 +48,25 @@ namespace osu.Game.Collections
|
||||
|
||||
private void collectionsChanged(IRealmCollection<BeatmapCollection> 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<Live<BeatmapCollection>> CreateOsuDrawable(Live<BeatmapCollection> 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The flow of <see cref="DrawableCollectionListItem"/>. Disables layout easing unless a drag is in progress.
|
||||
/// </summary>
|
||||
|
@ -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!;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="DrawableCollectionListItem"/>.
|
||||
/// </summary>
|
||||
@ -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);
|
||||
|
||||
/// <summary>
|
||||
/// The main content of the <see cref="DrawableCollectionListItem"/>.
|
||||
@ -57,10 +61,7 @@ namespace osu.Game.Collections
|
||||
{
|
||||
private readonly Live<BeatmapCollection> collection;
|
||||
|
||||
private ItemTextBox textBox = null!;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
public ItemTextBox TextBox { get; private set; } = null!;
|
||||
|
||||
public ItemContent(Live<BeatmapCollection> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -12,23 +12,28 @@ namespace osu.Game.Localisation.SkinComponents
|
||||
/// <summary>
|
||||
/// "Attribute"
|
||||
/// </summary>
|
||||
public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), "Attribute");
|
||||
public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), @"Attribute");
|
||||
|
||||
/// <summary>
|
||||
/// "The attribute to be displayed."
|
||||
/// </summary>
|
||||
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.");
|
||||
|
||||
/// <summary>
|
||||
/// "Template"
|
||||
/// </summary>
|
||||
public static LocalisableString Template => new TranslatableString(getKey(@"template"), "Template");
|
||||
public static LocalisableString Template => new TranslatableString(getKey(@"template"), @"Template");
|
||||
|
||||
/// <summary>
|
||||
/// "Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)."
|
||||
/// </summary>
|
||||
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}";
|
||||
/// <summary>
|
||||
/// "Max PP"
|
||||
/// </summary>
|
||||
public static LocalisableString MaxPP => new TranslatableString(getKey(@"max_pp"), @"Max PP");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -1,121 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#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<PerformanceBreakdown> 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<PerformanceAttributes> 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<IApplicableToScoreProcessor>())
|
||||
{
|
||||
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<HitResult> getPerfectHitResults(HitObject hitObject)
|
||||
{
|
||||
foreach (HitObject nested in hitObject.NestedHitObjects)
|
||||
yield return nested.Judgement.MaxResult;
|
||||
|
||||
yield return hitObject.Judgement.MaxResult;
|
||||
}
|
||||
}
|
||||
}
|
@ -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))));
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Rulesets.Difficulty.Utils
|
||||
{
|
||||
public static class DifficultyCalculationUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts BPM value into milliseconds
|
||||
/// </summary>
|
||||
/// <param name="bpm">Beats per minute</param>
|
||||
/// <param name="delimiter">Which rhythm delimiter to use, default is 1/4</param>
|
||||
/// <returns>BPM conveted to milliseconds</returns>
|
||||
public static double BPMToMilliseconds(double bpm, int delimiter = 4)
|
||||
{
|
||||
return 60000.0 / delimiter / bpm;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts milliseconds value into a BPM value
|
||||
/// </summary>
|
||||
/// <param name="ms">Milliseconds</param>
|
||||
/// <param name="delimiter">Which rhythm delimiter to use, default is 1/4</param>
|
||||
/// <returns>Milliseconds conveted to beats per minute</returns>
|
||||
public static double MillisecondsToBPM(double ms, int delimiter = 4)
|
||||
{
|
||||
return 60000.0 / (ms * delimiter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a S-shaped logistic function (https://en.wikipedia.org/wiki/Logistic_function)
|
||||
/// </summary>
|
||||
/// <param name="x">Value to calculate the function for</param>
|
||||
/// <param name="maxValue">Maximum value returnable by the function</param>
|
||||
/// <param name="multiplier">Growth rate of the function</param>
|
||||
/// <param name="midpointOffset">How much the function midpoint is offset from zero <paramref name="x"/></param>
|
||||
/// <returns>The output of logistic function of <paramref name="x"/></returns>
|
||||
public static double Logistic(double x, double midpointOffset, double multiplier, double maxValue = 1) => maxValue / (1 + Math.Exp(multiplier * (midpointOffset - x)));
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a S-shaped logistic function (https://en.wikipedia.org/wiki/Logistic_function)
|
||||
/// </summary>
|
||||
/// <param name="maxValue">Maximum value returnable by the function</param>
|
||||
/// <param name="exponent">Exponent</param>
|
||||
/// <returns>The output of logistic function</returns>
|
||||
public static double Logistic(double exponent, double maxValue = 1) => maxValue / (1 + Math.Exp(exponent));
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy osu!catch Hit-type, used for parsing Beatmaps.
|
||||
/// </summary>
|
||||
internal sealed class ConvertHit : ConvertHitObject, IHasPosition
|
||||
{
|
||||
public float X => Position.X;
|
||||
|
||||
public float Y => Position.Y;
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osuTK;
|
||||
using osu.Game.Audio;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
{
|
||||
/// <summary>
|
||||
/// A HitObjectParser to parse legacy osu!catch Beatmaps.
|
||||
/// </summary>
|
||||
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<IList<HitSampleInfo>> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy osu!catch Slider-type, used for parsing Beatmaps.
|
||||
/// </summary>
|
||||
internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition
|
||||
{
|
||||
public float X => Position.X;
|
||||
|
||||
public float Y => Position.Y;
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
}
|
||||
}
|
13
osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs
Normal file
13
osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs
Normal file
@ -0,0 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy "HitCircle" hit object type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only used for parsing beatmaps and not gameplay.
|
||||
/// </remarks>
|
||||
internal sealed class ConvertHitCircle : ConvertHitObject;
|
||||
}
|
@ -1,21 +1,34 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.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
|
||||
{
|
||||
/// <summary>
|
||||
/// A hit object only used for conversion, not actual gameplay.
|
||||
/// Represents a legacy hit object.
|
||||
/// </summary>
|
||||
internal abstract class ConvertHitObject : HitObject, IHasCombo
|
||||
/// <remarks>
|
||||
/// Only used for parsing beatmaps and not gameplay.
|
||||
/// </remarks>
|
||||
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;
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#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
|
||||
/// <summary>
|
||||
/// A HitObjectParser to parse legacy Beatmaps.
|
||||
/// </summary>
|
||||
public abstract class ConvertHitObjectParser : HitObjectParser
|
||||
public class ConvertHitObjectParser : HitObjectParser
|
||||
{
|
||||
/// <summary>
|
||||
/// The offset to apply to all time values.
|
||||
/// </summary>
|
||||
protected readonly double Offset;
|
||||
private readonly double offset;
|
||||
|
||||
/// <summary>
|
||||
/// The .osu format (beatmap) version.
|
||||
/// </summary>
|
||||
protected readonly int FormatVersion;
|
||||
private readonly int formatVersion;
|
||||
|
||||
protected bool FirstObject { get; private set; } = true;
|
||||
/// <summary>
|
||||
/// Whether the current hitobject is the first hitobject in the beatmap.
|
||||
/// </summary>
|
||||
private bool firstObject = true;
|
||||
|
||||
protected ConvertHitObjectParser(double offset, int formatVersion)
|
||||
/// <summary>
|
||||
/// The last parsed hitobject.
|
||||
/// </summary>
|
||||
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
|
||||
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
|
||||
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
|
||||
/// <returns>The hit object.</returns>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creats a legacy Slider-type hit object.
|
||||
@ -455,27 +470,51 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
/// <param name="repeatCount">The slider repeat count.</param>
|
||||
/// <param name="nodeSamples">The samples to be played when the slider nodes are hit. This includes the head and tail of the slider.</param>
|
||||
/// <returns>The hit object.</returns>
|
||||
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
|
||||
IList<IList<HitSampleInfo>> nodeSamples);
|
||||
private ConvertHitObject createSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
|
||||
IList<IList<HitSampleInfo>> 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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a legacy Spinner-type hit object.
|
||||
/// </summary>
|
||||
/// <param name="position">The position of the hit object.</param>
|
||||
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
|
||||
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
|
||||
/// <param name="duration">The spinner duration.</param>
|
||||
/// <returns>The hit object.</returns>
|
||||
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.
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a legacy Hold-type hit object.
|
||||
/// </summary>
|
||||
/// <param name="position">The position of the hit object.</param>
|
||||
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
|
||||
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
|
||||
/// <param name="duration">The hold duration.</param>
|
||||
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<HitSampleInfo> convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo)
|
||||
{
|
||||
@ -511,21 +550,19 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
/// <summary>
|
||||
/// An optional overriding filename which causes all bank/sample specifications to be ignored.
|
||||
/// </summary>
|
||||
public string Filename;
|
||||
public string? Filename;
|
||||
|
||||
/// <summary>
|
||||
/// The bank identifier to use for the base ("hitnormal") sample.
|
||||
/// Transferred to <see cref="HitSampleInfo.Bank"/> when appropriate.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public string BankForNormal;
|
||||
public string? BankForNormal;
|
||||
|
||||
/// <summary>
|
||||
/// The bank identifier to use for additions ("hitwhistle", "hitfinish", "hitclap").
|
||||
/// Transferred to <see cref="HitSampleInfo.Bank"/> when appropriate.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public string BankForAdditions;
|
||||
public string? BankForAdditions;
|
||||
|
||||
/// <summary>
|
||||
/// 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<LegacyHitSampleInfo>
|
||||
{
|
||||
public readonly int CustomSampleBank;
|
||||
@ -577,13 +612,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
IsLayered = isLayered;
|
||||
}
|
||||
|
||||
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
|
||||
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default,
|
||||
Optional<bool> newEditorAutoBank = default)
|
||||
=> With(newName, newBank, newVolume, newEditorAutoBank);
|
||||
|
||||
public virtual LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default,
|
||||
Optional<int> newCustomSampleBank = default,
|
||||
Optional<bool> 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<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default,
|
||||
Optional<bool> newEditorAutoBank = default, Optional<int> newCustomSampleBank = default, Optional<bool> 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<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default,
|
||||
Optional<int> newCustomSampleBank = default,
|
||||
Optional<bool> newIsLayered = default)
|
||||
public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default,
|
||||
Optional<bool> newEditorAutoBank = default, Optional<int> newCustomSampleBank = default, Optional<bool> newIsLayered = default)
|
||||
=> new FileHitSampleInfo(Filename, newVolume.GetOr(Volume));
|
||||
|
||||
public bool Equals(FileHitSampleInfo? other)
|
||||
|
@ -3,17 +3,18 @@
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
namespace osu.Game.Rulesets.Objects.Legacy
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy osu!catch Spinner-type, used for parsing Beatmaps.
|
||||
/// Legacy "Hold" hit object type. Generally only valid in the mania ruleset.
|
||||
/// </summary>
|
||||
internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition
|
||||
/// <remarks>
|
||||
/// Only used for parsing beatmaps and not gameplay.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#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
|
||||
/// <summary>
|
||||
/// Legacy "Slider" hit object type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only used for parsing beatmaps and not gameplay.
|
||||
/// </remarks>
|
||||
internal class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks
|
||||
{
|
||||
/// <summary>
|
||||
/// 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);
|
||||
|
@ -3,11 +3,14 @@
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
|
||||
namespace osu.Game.Rulesets.Objects.Legacy
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy osu!taiko Spinner-type, used for parsing Beatmaps.
|
||||
/// Legacy "Spinner" hit object type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only used for parsing beatmaps and not gameplay.
|
||||
/// </remarks>
|
||||
internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration
|
||||
{
|
||||
public double Duration { get; set; }
|
18
osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs
Normal file
18
osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs
Normal file
@ -0,0 +1,18 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy
|
||||
{
|
||||
/// <summary>
|
||||
/// A hit object from a legacy beatmap representation.
|
||||
/// </summary>
|
||||
public interface IHasLegacyHitObjectType
|
||||
{
|
||||
/// <summary>
|
||||
/// The hit object type.
|
||||
/// </summary>
|
||||
LegacyHitObjectType LegacyType { get; }
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Mania
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy osu!mania Hit-type, used for parsing Beatmaps.
|
||||
/// </summary>
|
||||
internal sealed class ConvertHit : ConvertHitObject, IHasXPosition
|
||||
{
|
||||
public float X { get; set; }
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osuTK;
|
||||
using osu.Game.Audio;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Mania
|
||||
{
|
||||
/// <summary>
|
||||
/// A HitObjectParser to parse legacy osu!mania Beatmaps.
|
||||
/// </summary>
|
||||
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<IList<HitSampleInfo>> 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.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;
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Mania
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy osu!mania Slider-type, used for parsing Beatmaps.
|
||||
/// </summary>
|
||||
internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition
|
||||
{
|
||||
public float X { get; set; }
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Mania
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy osu!mania Spinner-type, used for parsing Beatmaps.
|
||||
/// </summary>
|
||||
internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition
|
||||
{
|
||||
public double Duration { get; set; }
|
||||
|
||||
public double EndTime => StartTime + Duration;
|
||||
|
||||
public float X { get; set; }
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy osu! Hit-type, used for parsing Beatmaps.
|
||||
/// </summary>
|
||||
internal sealed class ConvertHit : ConvertHitObject, IHasPosition
|
||||
{
|
||||
public Vector2 Position { get; set; }
|
||||
|
||||
public float X => Position.X;
|
||||
|
||||
public float Y => Position.Y;
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osuTK;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Audio;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
||||
{
|
||||
/// <summary>
|
||||
/// A HitObjectParser to parse legacy osu! Beatmaps.
|
||||
/// </summary>
|
||||
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<IList<HitSampleInfo>> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy osu! Slider-type, used for parsing Beatmaps.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy osu! Spinner-type, used for parsing Beatmaps.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy osu!taiko Hit-type, used for parsing Beatmaps.
|
||||
/// </summary>
|
||||
internal sealed class ConvertHit : ConvertHitObject
|
||||
{
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osuTK;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Audio;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
|
||||
{
|
||||
/// <summary>
|
||||
/// A HitObjectParser to parse legacy osu!taiko Beatmaps.
|
||||
/// </summary>
|
||||
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<IList<HitSampleInfo>> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy osu!taiko Slider-type, used for parsing Beatmaps.
|
||||
/// </summary>
|
||||
internal sealed class ConvertSlider : Legacy.ConvertSlider
|
||||
{
|
||||
}
|
||||
}
|
@ -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
|
||||
/// </returns>
|
||||
public static double? CalculateAverageHitError(this IEnumerable<HitEvent> 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();
|
||||
}
|
||||
}
|
||||
|
@ -119,6 +119,11 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public long MaximumTotalScore { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum achievable combo.
|
||||
/// </summary>
|
||||
public int MaximumCombo { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum sum of accuracy-affecting judgements at the current point in time.
|
||||
/// </summary>
|
||||
@ -423,6 +428,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
MaximumResultCounts.AddRange(ScoreResultCounts);
|
||||
|
||||
MaximumTotalScore = TotalScore.Value;
|
||||
MaximumCombo = HighestCombo.Value;
|
||||
}
|
||||
|
||||
ScoreResultCounts.Clear();
|
||||
|
@ -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
|
||||
/// </summary>
|
||||
protected virtual void UpdateTernaryStates()
|
||||
{
|
||||
SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType<IHasComboInformation>(), h => h.NewCombo);
|
||||
if (SelectedItems.Any())
|
||||
SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType<IHasComboInformation>(), h => h.NewCombo);
|
||||
AutoSelectionBankEnabled.Value = SelectedItems.Count == 0;
|
||||
|
||||
var samplesInSelection = SelectedItems.SelectMany(enumerateAllSamples).ToArray();
|
||||
|
@ -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,
|
||||
|
@ -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<bool> contracted = new Bindable<bool>();
|
||||
|
||||
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)
|
||||
|
@ -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<HitObject> placement;
|
||||
private SelectionBlueprint<HitObject> placementBlueprint;
|
||||
|
||||
@ -118,9 +123,53 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
base.Update();
|
||||
|
||||
updateSamplePointContractedState();
|
||||
updateStacking();
|
||||
}
|
||||
|
||||
public Bindable<bool> SamplePointContracted = new Bindable<bool>();
|
||||
|
||||
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<HitObject> currentConcurrentObjects = new Stack<HitObject>();
|
||||
|
||||
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<SelectionBlueprint<HitObject>>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
|
||||
|
@ -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<Drawable> Content => zoomedContent;
|
||||
|
||||
/// <summary>
|
||||
/// The current zoom level of <see cref="ZoomableScrollContainer"/>.
|
||||
/// The current (final) zoom level of <see cref="ZoomableScrollContainer"/>.
|
||||
/// It may differ from <see cref="Zoom"/> during transitions.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
public Action? OnEditBeatmap;
|
||||
public Action? OnEditSkin;
|
||||
public Action? OnExit;
|
||||
public Action<UIEvent>? 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)
|
||||
{
|
||||
|
@ -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<MainMenuButton>? clickAction = null, params Key[] triggerKeys)
|
||||
public DailyChallengeButton(string sampleName, Color4 colour, Action<MainMenuButton, UIEvent>? 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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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<MainMenuButton>? clickAction;
|
||||
private readonly Action<MainMenuButton, UIEvent>? 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<MainMenuButton>? clickAction = null, params Key[] triggerKeys)
|
||||
public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action<MainMenuButton, UIEvent>? 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;
|
||||
|
@ -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(() =>
|
||||
|
@ -86,6 +86,7 @@ namespace osu.Game.Screens.Play
|
||||
public Action<bool> RestartRequested;
|
||||
|
||||
private bool isRestarting;
|
||||
private bool skipExitTransition;
|
||||
|
||||
private Bindable<bool> 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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to complete a user request to exit gameplay.
|
||||
/// Attempts to complete a user request to exit gameplay, with confirmation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item>This should only be called in response to a user interaction. Exiting is not guaranteed.</item>
|
||||
/// <item>This will interrupt any pending progression to the results screen, even if the transition has begun.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="showDialogFirst">
|
||||
/// Whether the pause or fail dialog should be shown before performing an exit.
|
||||
/// If <see langword="true"/> and a dialog is not yet displayed, the exit will be blocked and the relevant dialog will display instead.
|
||||
/// </param>
|
||||
/// <returns>Whether this call resulted in a final exit.</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to complete a user request to exit gameplay.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item>This should only be called in response to a user interaction. Exiting is not guaranteed.</item>
|
||||
/// <item>This will interrupt any pending progression to the results screen, even if the transition has begun.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <param name="skipTransition">Whether the exit should perform without a transition, because the screen had faded to black already.</param>
|
||||
/// <returns>Whether this call resulted in a final exit.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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())
|
||||
{
|
||||
|
@ -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<PlayerLoader> 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<PlayerLoader> 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()
|
||||
|
@ -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[]
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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<PerformanceBreakdown?> 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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -35,7 +35,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="11.5.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2024.1111.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2024.1115.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.1106.0" />
|
||||
<PackageReference Include="Sentry" Version="4.13.0" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
|
@ -17,6 +17,6 @@
|
||||
<MtouchInterpreter>-all</MtouchInterpreter>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1025.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1115.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user