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

Merge branch 'master' into no-gameplay-clock-editor-offset

This commit is contained in:
Dean Herbert 2022-08-26 20:25:21 +09:00 committed by GitHub
commit 90ff0864c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 518 additions and 22 deletions

View File

@ -77,6 +77,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return sliderCreatedFor(args);
});
AddAssert("samples exist", sliderSampleExist);
AddStep("undo", () => Editor.Undo());
AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && slider is not null && objectsRestored(circle1, slider, circle2));
}
@ -122,6 +124,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return sliderCreatedFor(args);
});
AddAssert("samples exist", sliderSampleExist);
AddAssert("merged slider matches first slider", () =>
{
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
@ -165,6 +169,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
(pos: circle1.Position, pathType: PathType.Linear),
(pos: circle2.Position, pathType: null)));
AddAssert("samples exist", sliderSampleExist);
AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner));
}
@ -209,5 +215,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return true;
}
private bool sliderSampleExist()
{
if (EditorBeatmap.SelectedHitObjects.Count != 1)
return false;
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return mergedSlider.Samples[0] is not null;
}
}
}

View File

@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
AddStep($"click context menu item \"{contextMenuText}\"", () =>
{
MenuItem item = visualiser.ContextMenuItems[1].Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
MenuItem item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
item?.Action?.Value();
});

View File

@ -0,0 +1,255 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSliderSplitting : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private ComposeBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
private Slider? slider;
private PathControlPointVisualiser? visualiser;
private const double split_gap = 100;
[Test]
public void TestBasicSplit()
{
double endTime = 0;
AddStep("add slider", () =>
{
slider = new Slider
{
Position = new Vector2(0, 50),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(400, 0)),
new PathControlPoint(new Vector2(400, 150))
})
};
EditorBeatmap.Add(slider);
endTime = slider.EndTime;
});
AddStep("select added slider", () =>
{
EditorBeatmap.SelectedHitObjects.Add(slider);
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
});
moveMouseToControlPoint(2);
AddStep("select control point", () =>
{
if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true;
});
addContextMenuItemStep("Split control point");
AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 2 &&
sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap,
(new Vector2(0, 50), PathType.PerfectCurve),
(new Vector2(150, 200), null),
(new Vector2(300, 50), null)
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], slider.StartTime, endTime + split_gap,
(new Vector2(300, 50), PathType.PerfectCurve),
(new Vector2(400, 50), null),
(new Vector2(400, 200), null)
));
AddStep("undo", () => Editor.Undo());
AddAssert("original slider restored", () => EditorBeatmap.HitObjects.Count == 1 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, endTime,
(new Vector2(0, 50), PathType.PerfectCurve),
(new Vector2(150, 200), null),
(new Vector2(300, 50), PathType.PerfectCurve),
(new Vector2(400, 50), null),
(new Vector2(400, 200), null)
));
}
[Test]
public void TestDoubleSplit()
{
double endTime = 0;
AddStep("add slider", () =>
{
slider = new Slider
{
Position = new Vector2(0, 50),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.Bezier),
new PathControlPoint(new Vector2(400, 0)),
new PathControlPoint(new Vector2(400, 150), PathType.Catmull),
new PathControlPoint(new Vector2(300, 200)),
new PathControlPoint(new Vector2(400, 250))
})
};
EditorBeatmap.Add(slider);
endTime = slider.EndTime;
});
AddStep("select added slider", () =>
{
EditorBeatmap.SelectedHitObjects.Add(slider);
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
});
moveMouseToControlPoint(2);
AddStep("select first control point", () =>
{
if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true;
});
moveMouseToControlPoint(4);
AddStep("select second control point", () =>
{
if (visualiser is not null) visualiser.Pieces[4].IsSelected.Value = true;
});
addContextMenuItemStep("Split 2 control points");
AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 3 &&
sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap,
(new Vector2(0, 50), PathType.PerfectCurve),
(new Vector2(150, 200), null),
(new Vector2(300, 50), null)
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], EditorBeatmap.HitObjects[0].GetEndTime() + split_gap, slider.StartTime - split_gap,
(new Vector2(300, 50), PathType.Bezier),
(new Vector2(400, 50), null),
(new Vector2(400, 200), null)
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[2], EditorBeatmap.HitObjects[1].GetEndTime() + split_gap, endTime + split_gap * 2,
(new Vector2(400, 200), PathType.Catmull),
(new Vector2(300, 250), null),
(new Vector2(400, 300), null)
));
}
[Test]
public void TestSplitRetainsHitsounds()
{
HitSampleInfo? sample = null;
AddStep("add slider", () =>
{
slider = new Slider
{
Position = new Vector2(0, 50),
LegacyLastTickOffset = 36, // This is necessary for undo to retain the sample control point
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(400, 0)),
new PathControlPoint(new Vector2(400, 150))
})
};
EditorBeatmap.Add(slider);
});
AddStep("add hitsounds", () =>
{
if (slider is null) return;
slider.SampleControlPoint.SampleBank = "soft";
slider.SampleControlPoint.SampleVolume = 70;
sample = new HitSampleInfo("hitwhistle");
slider.Samples.Add(sample);
});
AddStep("select added slider", () =>
{
EditorBeatmap.SelectedHitObjects.Add(slider);
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
});
moveMouseToControlPoint(2);
AddStep("select control point", () =>
{
if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true;
});
addContextMenuItemStep("Split control point");
AddAssert("sliders have hitsounds", hasHitsounds);
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]));
AddStep("remove first slider", () => EditorBeatmap.RemoveAt(0));
AddStep("undo", () => Editor.Undo());
AddAssert("sliders have hitsounds", hasHitsounds);
bool hasHitsounds() => sample is not null &&
EditorBeatmap.HitObjects.All(o => o.SampleControlPoint.SampleBank == "soft" &&
o.SampleControlPoint.SampleVolume == 70 &&
o.Samples.Contains(sample));
}
private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints)
{
if (!Precision.AlmostEquals(s.StartTime, startTime, 1) || !Precision.AlmostEquals(s.EndTime, endTime, 1)) return false;
int i = 0;
foreach ((Vector2 pos, PathType? pathType) in expectedControlPoints)
{
var controlPoint = s.Path.ControlPoints[i++];
if (!Precision.AlmostEquals(controlPoint.Position + s.Position, pos) || controlPoint.Type != pathType)
return false;
}
return true;
}
private void moveMouseToControlPoint(int index)
{
AddStep($"move mouse to control point {index}", () =>
{
if (slider is null || visualiser is null) return;
Vector2 position = slider.Path.ControlPoints[index].Position + slider.Position;
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position));
});
}
private void addContextMenuItemStep(string contextMenuText)
{
AddStep($"click context menu item \"{contextMenuText}\"", () =>
{
if (visualiser is null) return;
MenuItem? item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
item?.Action?.Value();
});
}
}
}

View File

@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private InputManager inputManager;
public Action<List<PathControlPoint>> RemoveControlPointsRequested;
public Action<List<PathControlPoint>> SplitControlPointsRequested;
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider snapProvider { get; set; }
@ -104,6 +105,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
return true;
}
private bool splitSelected()
{
List<PathControlPoint> controlPointsToSplitAt = Pieces.Where(p => p.IsSelected.Value && isSplittable(p)).Select(p => p.ControlPoint).ToList();
// Ensure that there are any points to be split
if (controlPointsToSplitAt.Count == 0)
return false;
changeHandler?.BeginChange();
SplitControlPointsRequested?.Invoke(controlPointsToSplitAt);
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 bool isSplittable(PathControlPointPiece p) =>
// A slider can only be split on control points which connect two different slider segments.
p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault();
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
@ -142,8 +166,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
case NotifyCollectionChangedAction.Remove:
foreach (var point in e.OldItems.Cast<PathControlPoint>())
{
Pieces.RemoveAll(p => p.ControlPoint == point);
Connections.RemoveAll(c => c.ControlPoint == point);
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
piece.RemoveAndDisposeImmediately();
foreach (var connection in Connections.Where(c => c.ControlPoint == point).ToArray())
connection.RemoveAndDisposeImmediately();
}
// If removing before the end of the path,
@ -322,25 +348,42 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (count == 0)
return null;
List<MenuItem> items = new List<MenuItem>();
var splittablePieces = selectedPieces.Where(isSplittable).ToList();
int splittableCount = splittablePieces.Count;
List<MenuItem> curveTypeItems = new List<MenuItem>();
if (!selectedPieces.Contains(Pieces[0]))
items.Add(createMenuItemForPathType(null));
curveTypeItems.Add(createMenuItemForPathType(null));
// todo: hide/disable items which aren't valid for selected points
items.Add(createMenuItemForPathType(PathType.Linear));
items.Add(createMenuItemForPathType(PathType.PerfectCurve));
items.Add(createMenuItemForPathType(PathType.Bezier));
items.Add(createMenuItemForPathType(PathType.Catmull));
curveTypeItems.Add(createMenuItemForPathType(PathType.Linear));
curveTypeItems.Add(createMenuItemForPathType(PathType.PerfectCurve));
curveTypeItems.Add(createMenuItemForPathType(PathType.Bezier));
curveTypeItems.Add(createMenuItemForPathType(PathType.Catmull));
return new MenuItem[]
var menuItems = new List<MenuItem>
{
new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => DeleteSelected()),
new OsuMenuItem("Curve type")
{
Items = items
Items = curveTypeItems
}
};
if (splittableCount > 0)
{
menuItems.Add(new OsuMenuItem($"Split {"control point".ToQuantity(splittableCount, splittableCount > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}",
MenuItemType.Destructive,
() => splitSelected()));
}
menuItems.Add(
new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}",
MenuItemType.Destructive,
() => DeleteSelected())
);
return menuItems.ToArray();
}
}

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
@ -111,7 +112,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true)
{
RemoveControlPointsRequested = removeControlPoints
RemoveControlPointsRequested = removeControlPoints,
SplitControlPointsRequested = splitControlPoints
});
base.OnSelected();
@ -249,6 +251,74 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
HitObject.Position += first;
}
private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt)
{
// Arbitrary gap in milliseconds to put between split slider pieces
const double split_gap = 100;
// Ensure that there are any points to be split
if (controlPointsToSplitAt.Count == 0)
return;
editorBeatmap.SelectedHitObjects.Clear();
foreach (var splitPoint in controlPointsToSplitAt)
{
if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type is null)
continue;
// Split off the section of slider before this control point so the remaining control points to split are in the latter part of the slider.
int index = controlPoints.IndexOf(splitPoint);
if (index <= 0)
continue;
// Extract the split portion and remove from the original slider.
var splitControlPoints = controlPoints.Take(index + 1).ToList();
controlPoints.RemoveRange(0, index);
// Turn the control points which were split off into a new slider.
var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone();
var newSlider = new Slider
{
StartTime = HitObject.StartTime,
Position = HitObject.Position + splitControlPoints[0].Position,
NewCombo = HitObject.NewCombo,
SampleControlPoint = samplePoint,
DifficultyControlPoint = difficultyPoint,
LegacyLastTickOffset = HitObject.LegacyLastTickOffset,
Samples = HitObject.Samples.Select(s => s.With()).ToList(),
RepeatCount = HitObject.RepeatCount,
NodeSamples = HitObject.NodeSamples.Select(n => (IList<HitSampleInfo>)n.Select(s => s.With()).ToList()).ToList(),
Path = new SliderPath(splitControlPoints.Select(o => new PathControlPoint(o.Position - splitControlPoints[0].Position, o == splitControlPoints[^1] ? null : o.Type)).ToArray())
};
// Increase the start time of the slider before adding the new slider so the new slider is immediately inserted at the correct index and internal state remains valid.
HitObject.StartTime += split_gap;
editorBeatmap.Add(newSlider);
HitObject.NewCombo = false;
HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance;
HitObject.StartTime += newSlider.SpanDuration;
// In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider.
if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON)
{
HitObject.Path.ExpectedDistance.Value = null;
}
}
// Once all required pieces have been split off, the original slider has the final split.
// As a final step, we must reset its control points to have an origin of (0,0).
Vector2 first = controlPoints[0].Position;
foreach (var c in controlPoints)
c.Position -= first;
HitObject.Position += first;
}
private void convertToStream()
{
if (editorBeatmap == null || beatDivisor == null)

View File

@ -371,6 +371,7 @@ namespace osu.Game.Rulesets.Osu.Edit
Position = firstHitObject.Position,
NewCombo = firstHitObject.NewCombo,
SampleControlPoint = firstHitObject.SampleControlPoint,
Samples = firstHitObject.Samples,
};
if (mergedHitObject.Path.ControlPoints.Count == 0)

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -314,15 +315,55 @@ namespace osu.Game.Tests.Rulesets.Scoring
}), Is.EqualTo(expectedScore).Within(0.5d));
}
#pragma warning disable CS0618
[Test]
public void TestLegacyComboIncrease()
{
Assert.That(HitResult.LegacyComboIncrease.IncreasesCombo(), Is.True);
Assert.That(HitResult.LegacyComboIncrease.BreaksCombo(), Is.False);
Assert.That(HitResult.LegacyComboIncrease.AffectsCombo(), Is.True);
Assert.That(HitResult.LegacyComboIncrease.AffectsAccuracy(), Is.False);
Assert.That(HitResult.LegacyComboIncrease.IsBasic(), Is.False);
Assert.That(HitResult.LegacyComboIncrease.IsTick(), Is.False);
Assert.That(HitResult.LegacyComboIncrease.IsBonus(), Is.False);
Assert.That(HitResult.LegacyComboIncrease.IsHit(), Is.True);
Assert.That(HitResult.LegacyComboIncrease.IsScorable(), Is.True);
Assert.That(HitResultExtensions.ALL_TYPES, Does.Not.Contain(HitResult.LegacyComboIncrease));
// Cannot be used to apply results.
Assert.Throws<ArgumentException>(() => scoreProcessor.ApplyBeatmap(new Beatmap
{
HitObjects = { new TestHitObject(HitResult.LegacyComboIncrease) }
}));
ScoreInfo testScore = new ScoreInfo
{
MaxCombo = 1,
Statistics = new Dictionary<HitResult, int>
{
{ HitResult.Great, 1 }
},
MaximumStatistics = new Dictionary<HitResult, int>
{
{ HitResult.Great, 1 },
{ HitResult.LegacyComboIncrease, 1 }
}
};
double totalScore = new TestScoreProcessor().ComputeFinalScore(ScoringMode.Standardised, testScore);
Assert.That(totalScore, Is.EqualTo(750_000)); // 500K from accuracy (100%), and 250K from combo (50%).
}
#pragma warning restore CS0618
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new System.NotImplementedException();
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new NotImplementedException();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new System.NotImplementedException();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new System.NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new System.NotImplementedException();
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
public override string Description => string.Empty;
public override string ShortName => string.Empty;
@ -352,5 +393,33 @@ namespace osu.Game.Tests.Rulesets.Scoring
this.maxResult = maxResult;
}
}
private class TestScoreProcessor : ScoreProcessor
{
protected override double DefaultAccuracyPortion => 0.5;
protected override double DefaultComboPortion => 0.5;
public TestScoreProcessor()
: base(new TestRuleset())
{
}
// ReSharper disable once MemberHidesStaticFromOuterClass
private class TestRuleset : Ruleset
{
protected override IEnumerable<HitResult> GetValidHitResults() => new[] { HitResult.Great };
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new NotImplementedException();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
public override string Description => string.Empty;
public override string ShortName => string.Empty;
}
}
}
}

View File

@ -119,8 +119,20 @@ namespace osu.Game.Rulesets.Scoring
[EnumMember(Value = "ignore_hit")]
[Order(12)]
IgnoreHit,
/// <summary>
/// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not affect the base score (does not affect accuracy).
/// </summary>
/// <remarks>
/// DO NOT USE.
/// </remarks>
[EnumMember(Value = "legacy_combo_increase")]
[Order(99)]
[Obsolete("Do not use.")]
LegacyComboIncrease = 99
}
#pragma warning disable CS0618
public static class HitResultExtensions
{
/// <summary>
@ -150,6 +162,7 @@ namespace osu.Game.Rulesets.Scoring
case HitResult.Perfect:
case HitResult.LargeTickHit:
case HitResult.LargeTickMiss:
case HitResult.LegacyComboIncrease:
return true;
default:
@ -161,13 +174,25 @@ namespace osu.Game.Rulesets.Scoring
/// Whether a <see cref="HitResult"/> affects the accuracy portion of the score.
/// </summary>
public static bool AffectsAccuracy(this HitResult result)
=> IsScorable(result) && !IsBonus(result);
{
// LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result.
if (result == HitResult.LegacyComboIncrease)
return false;
return IsScorable(result) && !IsBonus(result);
}
/// <summary>
/// Whether a <see cref="HitResult"/> is a non-tick and non-bonus result.
/// </summary>
public static bool IsBasic(this HitResult result)
=> IsScorable(result) && !IsTick(result) && !IsBonus(result);
{
// LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result.
if (result == HitResult.LegacyComboIncrease)
return false;
return IsScorable(result) && !IsTick(result) && !IsBonus(result);
}
/// <summary>
/// Whether a <see cref="HitResult"/> should be counted as a tick.
@ -225,12 +250,19 @@ namespace osu.Game.Rulesets.Scoring
/// <summary>
/// Whether a <see cref="HitResult"/> is scorable.
/// </summary>
public static bool IsScorable(this HitResult result) => result >= HitResult.Miss && result < HitResult.IgnoreMiss;
public static bool IsScorable(this HitResult result)
{
// LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output.
if (result == HitResult.LegacyComboIncrease)
return true;
return result >= HitResult.Miss && result < HitResult.IgnoreMiss;
}
/// <summary>
/// An array of all scorable <see cref="HitResult"/>s.
/// </summary>
public static readonly HitResult[] ALL_TYPES = ((HitResult[])Enum.GetValues(typeof(HitResult))).ToArray();
public static readonly HitResult[] ALL_TYPES = ((HitResult[])Enum.GetValues(typeof(HitResult))).Except(new[] { HitResult.LegacyComboIncrease }).ToArray();
/// <summary>
/// Whether a <see cref="HitResult"/> is valid within a given <see cref="HitResult"/> range.
@ -251,4 +283,5 @@ namespace osu.Game.Rulesets.Scoring
return result > minResult && result < maxResult;
}
}
#pragma warning restore CS0618
}

View File

@ -61,6 +61,11 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="result">The <see cref="JudgementResult"/> to apply.</param>
public void ApplyResult(JudgementResult result)
{
#pragma warning disable CS0618
if (result.Type == HitResult.LegacyComboIncrease)
throw new ArgumentException(@$"A {nameof(HitResult.LegacyComboIncrease)} hit result cannot be applied.");
#pragma warning restore CS0618
JudgedHits++;
lastAppliedResult = result;

View File

@ -536,6 +536,9 @@ namespace osu.Game.Rulesets.Scoring
{
extractScoringValues(scoreInfo.Statistics, out current, out maximum);
current.MaxCombo = scoreInfo.MaxCombo;
if (scoreInfo.MaximumStatistics.Count > 0)
extractScoringValues(scoreInfo.MaximumStatistics, out _, out maximum);
}
/// <summary>
@ -591,7 +594,8 @@ namespace osu.Game.Rulesets.Scoring
if (result.IsBonus())
current.BonusScore += count * Judgement.ToNumericResult(result);
else
if (result.AffectsAccuracy())
{
// The maximum result of this judgement if it wasn't a miss.
// E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT).