Merge branch 'master' into fl-opacity

This commit is contained in:
Dan Balasescu
2022-01-20 14:48:20 +09:00
674 changed files with 9837 additions and 20827 deletions

View File

@ -12,14 +12,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuDifficultyAttributes : DifficultyAttributes
{
[JsonProperty("aim_strain")]
public double AimStrain { get; set; }
[JsonProperty("aim_difficulty")]
public double AimDifficulty { get; set; }
[JsonProperty("speed_strain")]
public double SpeedStrain { get; set; }
[JsonProperty("speed_difficulty")]
public double SpeedDifficulty { get; set; }
[JsonProperty("flashlight_rating")]
public double FlashlightRating { get; set; }
[JsonProperty("flashlight_difficulty")]
public double FlashlightDifficulty { get; set; }
[JsonProperty("slider_factor")]
public double SliderFactor { get; set; }
@ -43,15 +43,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
foreach (var v in base.ToDatabaseAttributes())
yield return v;
yield return (ATTRIB_ID_AIM, AimStrain);
yield return (ATTRIB_ID_SPEED, SpeedStrain);
yield return (ATTRIB_ID_AIM, AimDifficulty);
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
yield return (ATTRIB_ID_STRAIN, StarRating);
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
if (ShouldSerializeFlashlightRating())
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightRating);
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
}
@ -60,18 +60,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
base.FromDatabaseAttributes(values);
AimStrain = values[ATTRIB_ID_AIM];
SpeedStrain = values[ATTRIB_ID_SPEED];
AimDifficulty = values[ATTRIB_ID_AIM];
SpeedDifficulty = values[ATTRIB_ID_SPEED];
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
StarRating = values[ATTRIB_ID_STRAIN];
FlashlightRating = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
StarRating = values[ATTRIB_ID_DIFFICULTY];
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
}
// Used implicitly by Newtonsoft.Json to not serialize flashlight property in some cases.
#region Newtonsoft.Json implicit ShouldSerialize() methods
// The properties in this region are used implicitly by Newtonsoft.Json to not serialise certain fields in some cases.
// They rely on being named exactly the same as the corresponding fields (casing included) and as such should NOT be renamed
// unless the fields are also renamed.
[UsedImplicitly]
public bool ShouldSerializeFlashlightRating() => Mods.Any(m => m is ModFlashlight);
#endregion
}
}

View File

@ -74,9 +74,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
StarRating = starRating,
Mods = mods,
AimStrain = aimRating,
SpeedStrain = speedRating,
FlashlightRating = flashlightRating,
AimDifficulty = aimRating,
SpeedDifficulty = speedRating,
FlashlightDifficulty = flashlightRating,
SliderFactor = sliderFactor,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6,

View File

@ -0,0 +1,26 @@
// 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 Newtonsoft.Json;
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuPerformanceAttributes : PerformanceAttributes
{
[JsonProperty("aim")]
public double Aim { get; set; }
[JsonProperty("speed")]
public double Speed { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty("flashlight")]
public double Flashlight { get; set; }
[JsonProperty("effective_miss_count")]
public double EffectiveMissCount { get; set; }
}
}

View File

@ -25,14 +25,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private int countMeh;
private int countMiss;
private int effectiveMissCount;
private double effectiveMissCount;
public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, attributes, score)
{
}
public override double Calculate(Dictionary<string, double> categoryRatings = null)
public override PerformanceAttributes Calculate()
{
mods = Score.Mods;
accuracy = Score.Accuracy;
@ -45,11 +45,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
// Custom multipliers for NoFail and SpunOut.
if (mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
if (mods.Any(m => m is OsuModSpunOut))
if (mods.Any(m => m is OsuModSpunOut) && totalHits > 0)
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
if (mods.Any(h => h is OsuModRelax))
@ -72,42 +71,35 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Math.Pow(flashlightValue, 1.1), 1.0 / 1.1
) * multiplier;
if (categoryRatings != null)
return new OsuPerformanceAttributes
{
categoryRatings.Add("Aim", aimValue);
categoryRatings.Add("Speed", speedValue);
categoryRatings.Add("Accuracy", accuracyValue);
categoryRatings.Add("Flashlight", flashlightValue);
categoryRatings.Add("OD", Attributes.OverallDifficulty);
categoryRatings.Add("AR", Attributes.ApproachRate);
categoryRatings.Add("Max Combo", Attributes.MaxCombo);
}
return totalValue;
Aim = aimValue,
Speed = speedValue,
Accuracy = accuracyValue,
Flashlight = flashlightValue,
EffectiveMissCount = effectiveMissCount,
Total = totalValue
};
}
private double computeAimValue()
{
double rawAim = Attributes.AimStrain;
double rawAim = Attributes.AimDifficulty;
if (mods.Any(m => m is OsuModTouchDevice))
rawAim = Math.Pow(rawAim, 0.8);
double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
// Longer maps are worth more.
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
aimValue *= lengthBonus;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount);
aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount);
// Combo scaling.
if (Attributes.MaxCombo > 0)
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
aimValue *= getComboScalingFactor();
double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33)
@ -136,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
}
aimValue *= accuracy;
// It is important to also consider accuracy difficulty when doing that.
// It is important to consider accuracy difficulty when scaling with accuracy.
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
return aimValue;
@ -144,20 +136,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeSpeedValue()
{
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0;
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
// Longer maps are worth more.
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
speedValue *= lengthBonus;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
// Combo scaling.
if (Attributes.MaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
speedValue *= getComboScalingFactor();
double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33)
@ -227,7 +216,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (!mods.Any(h => h is OsuModFlashlight))
return 0.0;
double rawFlashlight = Attributes.FlashlightRating;
double rawFlashlight = Attributes.FlashlightDifficulty;
if (mods.Any(m => m is OsuModTouchDevice))
rawFlashlight = Math.Pow(rawFlashlight, 0.8);
@ -236,11 +225,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
// Combo scaling.
if (Attributes.MaxCombo > 0)
flashlightValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
flashlightValue *= getComboScalingFactor();
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
@ -254,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue;
}
private int calculateEffectiveMissCount()
private double calculateEffectiveMissCount()
{
// Guess the number of misses + slider breaks from combo
double comboBasedMissCount = 0.0;
@ -266,12 +253,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
}
// Clamp misscount since it's derived from combo and can be higher than total hits and that breaks some calculations
// Clamp miss count since it's derived from combo and can be higher than total hits and that breaks some calculations
comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits);
return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount));
return Math.Max(countMiss, comboBasedMissCount);
}
private double getComboScalingFactor() => Attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
}

View File

@ -16,11 +16,9 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation;
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.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
@ -33,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public class PathControlPointPiece : BlueprintPiece<Slider>, IHasTooltip
{
public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
public Action<PathControlPoint> DragStarted;
public Action<DragEvent> DragInProgress;
public Action DragEnded;
public List<PathControlPoint> PointsInSegment;
public readonly BindableBool IsSelected = new BindableBool();
@ -42,12 +45,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private readonly Container marker;
private readonly Drawable markerRing;
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
[Resolved(CanBeNull = true)]
private IPositionSnapProvider snapProvider { get; set; }
[Resolved]
private OsuColour colours { get; set; }
@ -138,6 +135,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
updateMarkerDisplay();
}
// Used to pair up mouse down/drag events with their corresponding mouse up events,
// to avoid deselecting the piece by accident when the mouse up corresponding to the mouse down/drag fires.
private bool keepSelection;
protected override bool OnMouseDown(MouseDownEvent e)
{
if (RequestSelection == null)
@ -146,22 +147,41 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
switch (e.Button)
{
case MouseButton.Left:
// if control is pressed, do not do anything as the user may be adding to current selection
// or dragging all currently selected control points.
// if it isn't and the user's intent is to deselect, deselection will happen on mouse up.
if (e.ControlPressed && IsSelected.Value)
return true;
RequestSelection.Invoke(this, e);
keepSelection = true;
return true;
case MouseButton.Right:
if (!IsSelected.Value)
RequestSelection.Invoke(this, e);
keepSelection = true;
return false; // Allow context menu to show
}
return false;
}
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
protected override void OnMouseUp(MouseUpEvent e)
{
base.OnMouseUp(e);
private Vector2 dragStartPosition;
private PathType? dragPathType;
// ctrl+click deselects this piece, but only if this event
// wasn't immediately preceded by a matching mouse down or drag.
if (IsSelected.Value && e.ControlPressed && !keepSelection)
IsSelected.Value = false;
keepSelection = false;
}
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
protected override bool OnDragStart(DragStartEvent e)
{
@ -170,54 +190,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (e.Button == MouseButton.Left)
{
dragStartPosition = ControlPoint.Position;
dragPathType = PointsInSegment[0].Type;
changeHandler?.BeginChange();
DragStarted?.Invoke(ControlPoint);
keepSelection = true;
return true;
}
return false;
}
protected override void OnDrag(DragEvent e)
{
Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position).ToArray();
var oldPosition = slider.Position;
double oldStartTime = slider.StartTime;
protected override void OnDrag(DragEvent e) => DragInProgress?.Invoke(e);
if (ControlPoint == slider.Path.ControlPoints[0])
{
// Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account
var result = snapProvider?.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition);
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? e.ScreenSpaceMousePosition) - slider.Position;
slider.Position += movementDelta;
slider.StartTime = result?.Time ?? slider.StartTime;
// Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position -= movementDelta;
}
else
ControlPoint.Position = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
if (!slider.Path.HasValidLength)
{
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position = oldControlPoints[i];
slider.Position = oldPosition;
slider.StartTime = oldStartTime;
return;
}
// Maintain the path type in case it got defaulted to bezier at some point during the drag.
PointsInSegment[0].Type = dragPathType;
}
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
protected override void OnDragEnd(DragEndEvent e) => DragEnded?.Invoke();
private void cachePoints(Slider slider) => PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
@ -267,7 +250,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
switch (pathType)
{
case PathType.Catmull:
return colours.Seafoam;
return colours.SeaFoam;
case PathType.Bezier:
return colours.Pink;

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
@ -16,6 +17,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
@ -40,6 +42,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public Action<List<PathControlPoint>> RemoveControlPointsRequested;
[Resolved(CanBeNull = true)]
private IPositionSnapProvider snapProvider { get; set; }
public PathControlPointVisualiser(Slider slider, bool allowSelection)
{
this.slider = slider;
@ -64,6 +69,39 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
controlPoints.BindTo(slider.Path.ControlPoints);
}
/// <summary>
/// Selects the <see cref="PathControlPointPiece"/> corresponding to the given <paramref name="pathControlPoint"/>,
/// and deselects all other <see cref="PathControlPointPiece"/>s.
/// </summary>
public void SetSelectionTo(PathControlPoint pathControlPoint)
{
foreach (var p in Pieces)
p.IsSelected.Value = p.ControlPoint == pathControlPoint;
}
/// <summary>
/// Delete all visually selected <see cref="PathControlPoint"/>s.
/// </summary>
/// <returns></returns>
public bool DeleteSelected()
{
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
// Ensure that there are any points to be deleted
if (toRemove.Count == 0)
return false;
changeHandler?.BeginChange();
RemoveControlPointsRequested?.Invoke(toRemove);
changeHandler?.EndChange();
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
return true;
}
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
@ -87,7 +125,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Pieces.Add(new PathControlPointPiece(slider, point).With(d =>
{
if (allowSelection)
d.RequestSelection = selectPiece;
d.RequestSelection = selectionRequested;
d.DragStarted = dragStarted;
d.DragInProgress = dragInProgress;
d.DragEnded = dragEnded;
}));
Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i));
@ -119,6 +161,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override bool OnClick(ClickEvent e)
{
if (Pieces.Any(piece => piece.IsHovered))
return false;
foreach (var piece in Pieces)
{
piece.IsSelected.Value = false;
@ -142,15 +187,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
}
private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e)
private void selectionRequested(PathControlPointPiece piece, MouseButtonEvent e)
{
if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
piece.IsSelected.Toggle();
else
{
foreach (var p in Pieces)
p.IsSelected.Value = p == piece;
}
SetSelectionTo(piece.ControlPoint);
}
/// <summary>
@ -184,25 +226,87 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
public bool DeleteSelected()
{
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
#region Drag handling
// Ensure that there are any points to be deleted
if (toRemove.Count == 0)
return false;
private Vector2[] dragStartPositions;
private PathType?[] dragPathTypes;
private int draggedControlPointIndex;
private HashSet<PathControlPoint> selectedControlPoints;
private void dragStarted(PathControlPoint controlPoint)
{
dragStartPositions = slider.Path.ControlPoints.Select(point => point.Position).ToArray();
dragPathTypes = slider.Path.ControlPoints.Select(point => point.Type).ToArray();
draggedControlPointIndex = slider.Path.ControlPoints.IndexOf(controlPoint);
selectedControlPoints = new HashSet<PathControlPoint>(Pieces.Where(piece => piece.IsSelected.Value).Select(piece => piece.ControlPoint));
Debug.Assert(draggedControlPointIndex >= 0);
changeHandler?.BeginChange();
RemoveControlPointsRequested?.Invoke(toRemove);
changeHandler?.EndChange();
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
return true;
}
private void dragInProgress(DragEvent e)
{
Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position).ToArray();
var oldPosition = slider.Position;
double oldStartTime = slider.StartTime;
if (selectedControlPoints.Contains(slider.Path.ControlPoints[0]))
{
// Special handling for selections containing head control point - the position of the slider changes which means the snapped position and time have to be taken into account
Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
var result = snapProvider?.SnapScreenSpacePositionToValidTime(newHeadPosition);
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - slider.Position;
slider.Position += movementDelta;
slider.StartTime = result?.Time ?? slider.StartTime;
for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
{
var controlPoint = slider.Path.ControlPoints[i];
// Since control points are relative to the position of the slider, all points that are _not_ selected
// need to be offset _back_ by the delta corresponding to the movement of the head point.
// All other selected control points (if any) will move together with the head point
// (and so they will not move at all, relative to each other).
if (!selectedControlPoints.Contains(controlPoint))
controlPoint.Position -= movementDelta;
}
}
else
{
for (int i = 0; i < controlPoints.Count; ++i)
{
var controlPoint = controlPoints[i];
if (selectedControlPoints.Contains(controlPoint))
controlPoint.Position = dragStartPositions[i] + (e.MousePosition - e.MouseDownPosition);
}
}
// Snap the path to the current beat divisor before checking length validity.
slider.SnapTo(snapProvider);
if (!slider.Path.HasValidLength)
{
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position = oldControlPoints[i];
slider.Position = oldPosition;
slider.StartTime = oldStartTime;
// Snap the path length again to undo the invalid length.
slider.SnapTo(snapProvider);
return;
}
// Maintain the path types in case they got defaulted to bezier at some point during the drag.
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Type = dragPathTypes[i];
}
private void dragEnded() => changeHandler?.EndChange();
#endregion
public MenuItem[] ContextMenuItems
{
get

View File

@ -9,7 +9,6 @@ using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -50,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load()
{
InternalChildren = new Drawable[]
{

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@ -81,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
controlPoints.BindTo(HitObject.Path.ControlPoints);
pathVersion.BindTo(HitObject.Path.Version);
pathVersion.BindValueChanged(_ => updatePath());
pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject));
BodyPiece.UpdateFrom(HitObject);
}
@ -140,7 +139,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case MouseButton.Left:
if (e.ControlPressed && IsSelected)
{
placementControlPointIndex = addControlPoint(e.MousePosition);
changeHandler?.BeginChange();
placementControlPoint = addControlPoint(e.MousePosition);
ControlPointVisualiser?.SetSelectionTo(placementControlPoint);
return true; // Stop input from being handled and modifying the selection
}
@ -150,31 +151,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return false;
}
private int? placementControlPointIndex;
[CanBeNull]
private PathControlPoint placementControlPoint;
protected override bool OnDragStart(DragStartEvent e)
{
if (placementControlPointIndex != null)
{
changeHandler?.BeginChange();
return true;
}
return false;
}
protected override bool OnDragStart(DragStartEvent e) => placementControlPoint != null;
protected override void OnDrag(DragEvent e)
{
Debug.Assert(placementControlPointIndex != null);
HitObject.Path.ControlPoints[placementControlPointIndex.Value].Position = e.MousePosition - HitObject.Position;
if (placementControlPoint != null)
placementControlPoint.Position = e.MousePosition - HitObject.Position;
}
protected override void OnDragEnd(DragEndEvent e)
protected override void OnMouseUp(MouseUpEvent e)
{
if (placementControlPointIndex != null)
if (placementControlPoint != null)
{
placementControlPointIndex = null;
placementControlPoint = null;
changeHandler?.EndChange();
}
}
@ -193,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return false;
}
private int addControlPoint(Vector2 position)
private PathControlPoint addControlPoint(Vector2 position)
{
position -= HitObject.Position;
@ -211,10 +203,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
// Move the control points from the insertion index onwards to make room for the insertion
controlPoints.Insert(insertionIndex, new PathControlPoint { Position = position });
var pathControlPoint = new PathControlPoint { Position = position };
return insertionIndex;
// Move the control points from the insertion index onwards to make room for the insertion
controlPoints.Insert(insertionIndex, pathControlPoint);
HitObject.SnapTo(composer);
return pathControlPoint;
}
private void removeControlPoints(List<PathControlPoint> toRemove)
@ -233,7 +229,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
controlPoints.Remove(c);
}
// If there are 0 or 1 remaining control points, the slider is in a degenerate (single point) form and should be deleted
// Snap the slider to the current beat divisor before checking length validity.
HitObject.SnapTo(composer);
// If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted
if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength)
{
placementHandler?.Delete(HitObject);
@ -248,12 +247,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
HitObject.Position += first;
}
private void updatePath()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
editorBeatmap?.Update(HitObject);
}
private void convertToStream()
{
if (editorBeatmap == null || changeHandler == null || beatDivisor == null)

View File

@ -1,15 +1,20 @@
// 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 enable
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Extensions;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@ -17,6 +22,9 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuSelectionHandler : EditorSelectionHandler
{
[Resolved(CanBeNull = true)]
private IPositionSnapProvider? positionSnapProvider { get; set; }
/// <summary>
/// During a transform, the initial origin is stored so it can be used throughout the operation.
/// </summary>
@ -26,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit
/// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation.
/// </summary>
private List<PathType?> referencePathTypes;
private List<PathType?>? referencePathTypes;
protected override void OnSelectionChanged()
{
@ -84,18 +92,28 @@ namespace osu.Game.Rulesets.Osu.Edit
return true;
}
public override bool HandleFlip(Direction direction)
public override bool HandleFlip(Direction direction, bool flipOverOrigin)
{
var hitObjects = selectedMovableObjects;
var selectedObjectsQuad = getSurroundingQuad(hitObjects);
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : getSurroundingQuad(hitObjects);
bool didFlip = false;
foreach (var h in hitObjects)
{
h.Position = GetFlippedPosition(direction, selectedObjectsQuad, h.Position);
var flippedPosition = GetFlippedPosition(direction, flipQuad, h.Position);
if (!Precision.AlmostEquals(flippedPosition, h.Position))
{
h.Position = flippedPosition;
didFlip = true;
}
if (h is Slider slider)
{
didFlip = true;
foreach (var point in slider.Path.ControlPoints)
{
point.Position = new Vector2(
@ -106,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Edit
}
}
return true;
return didFlip;
}
public override bool HandleScale(Vector2 scale, Anchor reference)
@ -186,6 +204,10 @@ namespace osu.Game.Rulesets.Osu.Edit
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
slider.Path.ControlPoints[i].Type = referencePathTypes[i];
// Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks.
slider.SnapTo(positionSnapProvider);
//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
@ -195,6 +217,9 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var point in slider.Path.ControlPoints)
point.Position = oldControlPoints.Dequeue();
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
slider.SnapTo(positionSnapProvider);
}
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Spun Out";
public override string Acronym => "SO";
public override IconUsage? Icon => OsuIcon.ModSpunout;
public override IconUsage? Icon => OsuIcon.ModSpunOut;
public override ModType Type => ModType.Automation;
public override string Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9;

View File

@ -10,7 +10,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@ -69,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load()
{
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;

View File

@ -65,8 +65,8 @@ namespace osu.Game.Rulesets.Osu.Objects
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired
? new SpinnerTick { StartTime = startTime }
: new SpinnerBonusTick { StartTime = startTime });
? new SpinnerTick { StartTime = startTime, Position = Position }
: new SpinnerBonusTick { StartTime = startTime, Position = Position });
}
}

View File

@ -3,15 +3,13 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class SpinnerBackgroundLayer : SpinnerFill
{
[BackgroundDependencyLoader]
private void load(OsuColour colours, DrawableHitObject drawableHitObject)
private void load()
{
Disc.Alpha = 0;
Anchor = Anchor.Centre;

View File

@ -0,0 +1,52 @@
// 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.Audio.Track;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
#nullable enable
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
internal class KiaiFlashingDrawable : BeatSyncedContainer
{
private readonly Drawable flashingDrawable;
private const float flash_opacity = 0.3f;
public KiaiFlashingDrawable(Func<Drawable?> creationFunc)
{
AutoSizeAxes = Axes.Both;
Children = new[]
{
(creationFunc.Invoke() ?? Empty()).With(d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
}),
flashingDrawable = (creationFunc.Invoke() ?? Empty()).With(d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
d.Alpha = 0;
d.Blending = BlendingParameters.Additive;
})
};
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
if (!effectPoint.KiaiMode)
return;
flashingDrawable
.FadeTo(flash_opacity)
.Then()
.FadeOut(timingPoint.BeatLength * 0.75f);
}
}
}

View File

@ -1,61 +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.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
internal class KiaiFlashingSprite : BeatSyncedContainer
{
private readonly Sprite mainSprite;
private readonly Sprite flashingSprite;
public Texture Texture
{
set
{
mainSprite.Texture = value;
flashingSprite.Texture = value;
}
}
private const float flash_opacity = 0.3f;
public KiaiFlashingSprite()
{
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
mainSprite = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
flashingSprite = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Blending = BlendingParameters.Additive,
}
};
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
if (!effectPoint.KiaiMode)
return;
flashingSprite
.FadeTo(flash_opacity)
.Then()
.FadeOut(timingPoint.BeatLength * 0.75f);
}
}
}

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private GameplayState gameplayState { get; set; }
[BackgroundDependencyLoader]
private void load(ISkinSource skin, OsuColour colours)
private void load(ISkinSource skin)
{
var texture = skin.GetTexture("star2");
var starBreakAdditive = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.StarBreakAdditive)?.Value ?? new Color4(255, 182, 193, 255);

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@ -68,13 +69,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
// the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin).
Texture overlayTexture = getTextureWithFallback("overlay");
InternalChildren = new[]
{
hitCircleSprite = new KiaiFlashingSprite
hitCircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = baseTexture })
{
Texture = baseTexture,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
@ -82,9 +81,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = hitCircleOverlay = new KiaiFlashingSprite
Child = hitCircleOverlay = new KiaiFlashingDrawable(() => getAnimationWithFallback(@"overlay", 1000 / 2d))
{
Texture = overlayTexture,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
@ -126,6 +124,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return tex ?? skin.GetTexture($"hitcircle{name}");
}
Drawable getAnimationWithFallback(string name, double frameLength)
{
Drawable animation = null;
if (!string.IsNullOrEmpty(priorityLookup))
{
animation = skin.GetAnimation($"{priorityLookup}{name}", true, true, frameLength: frameLength);
if (!allowFallback)
return animation;
}
return animation ?? skin.GetAnimation($"hitcircle{name}", true, true, frameLength: frameLength);
}
}
protected override void LoadComplete()

View File

@ -12,6 +12,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
CursorExpand,
CursorRotate,
HitCircleOverlayAboveNumber,
// ReSharper disable once IdentifierTypo
HitCircleOverlayAboveNumer, // Some old skins will have this typo
SpinnerFrequencyModulate,
SpinnerNoBlink

View File

@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
pointGrid.Content = points;
if (score.HitEvents == null || score.HitEvents.Count == 0)
if (score.HitEvents.Count == 0)
return;
// Todo: This should probably not be done like this.

View File

@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private OsuConfigManager config { get; set; }
[BackgroundDependencyLoader(true)]
private void load(OsuConfigManager config, OsuRulesetConfigManager rulesetConfig)
private void load(OsuRulesetConfigManager rulesetConfig)
{
rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorTrail, showTrail);
}