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

Merge branch 'ppy:master' into 1hour-song-multi

This commit is contained in:
Shin Morisawa 2024-11-14 16:32:24 +09:00 committed by GitHub
commit 53fcb494ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 430 additions and 720 deletions

View File

@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Mania.Edit
base.Update(); base.Update();
if (screenWithTimeline?.TimelineArea.Timeline != null) if (screenWithTimeline?.TimelineArea.Timeline != null)
drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom / 2; drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom.Value / 2;
} }
} }
} }

View File

@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -23,7 +20,6 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko;
@ -35,15 +31,11 @@ namespace osu.Game.Tests.Visual.SongSelect
[TestFixture] [TestFixture]
public partial class TestSceneBeatmapInfoWedge : OsuTestScene public partial class TestSceneBeatmapInfoWedge : OsuTestScene
{ {
private RulesetStore rulesets; [Resolved]
private TestBeatmapInfoWedge infoWedge; private RulesetStore rulesets { get; set; } = null!;
private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
[BackgroundDependencyLoader] private TestBeatmapInfoWedge infoWedge = null!;
private void load(RulesetStore rulesets) private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
{
this.rulesets = rulesets;
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
@ -156,7 +148,7 @@ namespace osu.Game.Tests.Visual.SongSelect
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm }); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
OsuModDoubleTime doubleTime = null; OsuModDoubleTime doubleTime = null!;
selectBeatmap(beatmap); selectBeatmap(beatmap);
checkDisplayedBPM($"{bpm}"); checkDisplayedBPM($"{bpm}");
@ -173,7 +165,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[TestCase(120, 120.4, null, "120")] [TestCase(120, 120.4, null, "120")]
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")] [TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
[TestCase(120, 120.4, "DT", "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); IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
@ -203,7 +195,7 @@ namespace osu.Game.Tests.Visual.SongSelect
double drain = beatmap.CalculateDrainLength(); double drain = beatmap.CalculateDrainLength();
beatmap.BeatmapInfo.Length = drain; beatmap.BeatmapInfo.Length = drain;
OsuModDoubleTime doubleTime = null; OsuModDoubleTime doubleTime = null!;
selectBeatmap(beatmap); selectBeatmap(beatmap);
checkDisplayedLength(drain); checkDisplayedLength(drain);
@ -221,14 +213,15 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep($"check map drain ({displayedLength})", () => 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(); return label.Statistic.Content == displayedLength.ToString();
}); });
} }
private void setRuleset(RulesetInfo rulesetInfo) private void setRuleset(RulesetInfo rulesetInfo)
{ {
Container containerBefore = null; Container? containerBefore = null;
AddStep("set ruleset", () => AddStep("set ruleset", () =>
{ {
@ -242,9 +235,9 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); 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", () => AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
{ {
@ -307,11 +300,6 @@ namespace osu.Game.Tests.Visual.SongSelect
public new WedgeInfoText Info => base.Info; public new WedgeInfoText Info => base.Info;
} }
private class TestHitObject : ConvertHitObject, IHasPosition private class TestHitObject : ConvertHitObject;
{
public float X => 0;
public float Y => 0;
public Vector2 Position { get; } = Vector2.Zero;
}
} }
} }

View File

@ -14,9 +14,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2 namespace osu.Game.Tests.Visual.SongSelectV2
{ {
@ -209,11 +207,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
public new WedgeInfoText? Info => base.Info; public new WedgeInfoText? Info => base.Info;
} }
private class TestHitObject : ConvertHitObject, IHasPosition private class TestHitObject : ConvertHitObject;
{
public float X => 0;
public float Y => 0;
public Vector2 Position { get; } = Vector2.Zero;
}
} }
} }

View File

@ -159,6 +159,23 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("check star rating is 2", getText, () => Is.EqualTo("Star Rating: 2.00")); 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 string getText() => text.ChildrenOfType<SpriteText>().Single().Text.ToString();
private class TestRuleset : Ruleset private class TestRuleset : Ruleset

View File

@ -4,12 +4,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures;
using osu.Framework.Lists; using osu.Framework.Lists;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Threading; using osu.Framework.Threading;
@ -18,7 +21,11 @@ using osu.Game.Database;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Storyboards;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -237,10 +244,37 @@ namespace osu.Game.Beatmaps
var ruleset = rulesetInfo.CreateInstance(); var ruleset = rulesetInfo.CreateInstance();
Debug.Assert(ruleset != null); Debug.Assert(ruleset != null);
var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); PlayableCachedWorkingBeatmap workingBeatmap = new PlayableCachedWorkingBeatmap(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo));
var attributes = calculator.Calculate(key.OrderedMods, cancellationToken); 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) catch (OperationCanceledException)
{ {
@ -276,7 +310,6 @@ namespace osu.Game.Beatmaps
{ {
public readonly BeatmapInfo BeatmapInfo; public readonly BeatmapInfo BeatmapInfo;
public readonly RulesetInfo Ruleset; public readonly RulesetInfo Ruleset;
public readonly Mod[] OrderedMods; public readonly Mod[] OrderedMods;
public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo? ruleset, IEnumerable<Mod>? mods) public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo? ruleset, IEnumerable<Mod>? mods)
@ -317,5 +350,42 @@ namespace osu.Game.Beatmaps
CancellationToken = cancellationToken; 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);
}
} }
} }

View File

@ -38,8 +38,7 @@ namespace osu.Game.Beatmaps.Formats
internal static RulesetStore? RulesetStore; internal static RulesetStore? RulesetStore;
private Beatmap beatmap = null!; private Beatmap beatmap = null!;
private ConvertHitObjectParser parser = null!;
private ConvertHitObjectParser? parser;
private LegacySampleBank defaultSampleBank; private LegacySampleBank defaultSampleBank;
private int defaultSampleVolume = 100; private int defaultSampleVolume = 100;
@ -80,6 +79,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
this.beatmap = beatmap; this.beatmap = beatmap;
this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion; this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion;
parser = new ConvertHitObjectParser(getOffsetTime(), FormatVersion);
applyLegacyDefaults(this.beatmap.BeatmapInfo); applyLegacyDefaults(this.beatmap.BeatmapInfo);
@ -162,7 +162,8 @@ namespace osu.Game.Beatmaps.Formats
{ {
if (hitObject is IHasRepeats hasRepeats) 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(); hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
@ -175,7 +176,8 @@ namespace osu.Game.Beatmaps.Formats
} }
else 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(); hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
} }
} }
@ -263,29 +265,7 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"Mode": case @"Mode":
int rulesetID = Parsing.ParseInt(pair.Value); beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(Parsing.ParseInt(pair.Value)) ?? throw new ArgumentException("Ruleset is not available locally.");
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;
}
break; break;
case @"LetterboxInBreaks": case @"LetterboxInBreaks":
@ -617,17 +597,10 @@ namespace osu.Game.Beatmaps.Formats
private void handleHitObject(string line) 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); var obj = parser.Parse(line);
obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
if (obj != null) beatmap.HitObjects.Add(obj);
{
obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
beatmap.HitObjects.Add(obj);
}
} }
private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0); private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0);

View File

@ -6,7 +6,7 @@ using System;
namespace osu.Game.Beatmaps.Legacy namespace osu.Game.Beatmaps.Legacy
{ {
[Flags] [Flags]
internal enum LegacyHitObjectType public enum LegacyHitObjectType
{ {
Circle = 1, Circle = 1,
Slider = 1 << 1, Slider = 1 << 1,

View File

@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using JetBrains.Annotations;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
@ -25,30 +22,34 @@ namespace osu.Game.Beatmaps
/// The difficulty attributes computed for the given beatmap. /// 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. /// Might not be available if the star difficulty is associated with a beatmap that's not locally available.
/// </summary> /// </summary>
[CanBeNull] public readonly DifficultyAttributes? DifficultyAttributes;
public readonly DifficultyAttributes Attributes;
/// <summary> /// <summary>
/// Creates a <see cref="StarDifficulty"/> structure based on <see cref="DifficultyAttributes"/> computed /// The performance attributes computed for a perfect score on the given beatmap.
/// by a <see cref="DifficultyCalculator"/>. /// Might not be available if the star difficulty is associated with a beatmap that's not locally available.
/// </summary> /// </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; Stars = double.IsFinite(difficulty.StarRating) ? difficulty.StarRating : 0;
MaxCombo = attributes.MaxCombo; MaxCombo = difficulty.MaxCombo;
Attributes = attributes; DifficultyAttributes = difficulty;
PerformanceAttributes = performance;
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
} }
/// <summary> /// <summary>
/// Creates a <see cref="StarDifficulty"/> structure with a pre-populated star difficulty and max combo /// 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> /// </summary>
public StarDifficulty(double starDifficulty, int maxCombo) public StarDifficulty(double starDifficulty, int maxCombo)
{ {
Stars = double.IsFinite(starDifficulty) ? starDifficulty : 0; Stars = double.IsFinite(starDifficulty) ? starDifficulty : 0;
MaxCombo = maxCombo; MaxCombo = maxCombo;
Attributes = null;
} }
public DifficultyRating DifficultyRating => GetDifficultyRating(Stars); public DifficultyRating DifficultyRating => GetDifficultyRating(Stars);

View File

@ -12,23 +12,28 @@ namespace osu.Game.Localisation.SkinComponents
/// <summary> /// <summary>
/// "Attribute" /// "Attribute"
/// </summary> /// </summary>
public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), "Attribute"); public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), @"Attribute");
/// <summary> /// <summary>
/// "The attribute to be displayed." /// "The attribute to be displayed."
/// </summary> /// </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> /// <summary>
/// "Template" /// "Template"
/// </summary> /// </summary>
public static LocalisableString Template => new TranslatableString(getKey(@"template"), "Template"); public static LocalisableString Template => new TranslatableString(getKey(@"template"), @"Template");
/// <summary> /// <summary>
/// "Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)." /// "Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)."
/// </summary> /// </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)."); 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}";
} }
} }

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View 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;
}

View File

@ -1,21 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Objects.Legacy namespace osu.Game.Rulesets.Objects.Legacy
{ {
/// <summary> /// <summary>
/// A hit object only used for conversion, not actual gameplay. /// Represents a legacy hit object.
/// </summary> /// </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 bool NewCombo { get; set; }
public int ComboOffset { 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(); public override Judgement CreateJudgement() => new IgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osuTK; using osuTK;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System; using System;
@ -11,7 +9,6 @@ using System.IO;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Audio; using osu.Game.Audio;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
@ -24,24 +21,32 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <summary> /// <summary>
/// A HitObjectParser to parse legacy Beatmaps. /// A HitObjectParser to parse legacy Beatmaps.
/// </summary> /// </summary>
public abstract class ConvertHitObjectParser : HitObjectParser public class ConvertHitObjectParser : HitObjectParser
{ {
/// <summary> /// <summary>
/// The offset to apply to all time values. /// The offset to apply to all time values.
/// </summary> /// </summary>
protected readonly double Offset; private readonly double offset;
/// <summary> /// <summary>
/// The .osu format (beatmap) version. /// The .osu format (beatmap) version.
/// </summary> /// </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; this.offset = offset;
FormatVersion = formatVersion; this.formatVersion = formatVersion;
} }
public override HitObject Parse(string text) public override HitObject Parse(string text)
@ -49,11 +54,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
string[] split = text.Split(','); string[] split = text.Split(',');
Vector2 pos = 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(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)); : 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]); 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 soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]);
var bankInfo = new SampleBankInfo(); var bankInfo = new SampleBankInfo();
HitObject result = null; ConvertHitObject? result = null;
if (type.HasFlag(LegacyHitObjectType.Circle)) if (type.HasFlag(LegacyHitObjectType.Circle))
{ {
result = CreateHit(pos, combo, comboOffset); result = createHitCircle(pos, combo, comboOffset);
if (split.Length > 5) if (split.Length > 5)
readCustomSampleBanks(split[5], bankInfo); readCustomSampleBanks(split[5], bankInfo);
@ -145,13 +150,13 @@ namespace osu.Game.Rulesets.Objects.Legacy
for (int i = 0; i < nodes; i++) for (int i = 0; i < nodes; i++)
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[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)) 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) if (split.Length > 6)
readCustomSampleBanks(split[6], bankInfo); readCustomSampleBanks(split[6], bankInfo);
@ -169,18 +174,19 @@ namespace osu.Game.Rulesets.Objects.Legacy
readCustomSampleBanks(string.Join(':', ss.Skip(1)), bankInfo); readCustomSampleBanks(string.Join(':', ss.Skip(1)), bankInfo);
} }
result = CreateHold(pos, combo, comboOffset, endTime + Offset - startTime); result = createHold(pos, endTime + offset - startTime);
} }
if (result == null) if (result == null)
throw new InvalidDataException($"Unknown hit object type: {split[3]}"); throw new InvalidDataException($"Unknown hit object type: {split[3]}");
result.StartTime = startTime; result.StartTime = startTime;
result.LegacyType = type;
if (result.Samples.Count == 0) if (result.Samples.Count == 0)
result.Samples = convertSoundType(soundType, bankInfo); result.Samples = convertSoundType(soundType, bankInfo);
FirstObject = false; firstObject = false;
return result; return result;
} }
@ -200,10 +206,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (!Enum.IsDefined(addBank)) if (!Enum.IsDefined(addBank))
addBank = LegacySampleBank.Normal; addBank = LegacySampleBank.Normal;
string stringBank = bank.ToString().ToLowerInvariant(); string? stringBank = bank.ToString().ToLowerInvariant();
string? stringAddBank = addBank.ToString().ToLowerInvariant();
if (stringBank == @"none") if (stringBank == @"none")
stringBank = null; stringBank = null;
string stringAddBank = addBank.ToString().ToLowerInvariant();
if (stringAddBank == @"none") if (stringAddBank == @"none")
{ {
@ -357,7 +364,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
{ {
int endPointLength = endPoint == null ? 0 : 1; int endPointLength = endPoint == null ? 0 : 1;
if (FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) if (formatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
{ {
if (vertices.Length + endPointLength != 3) if (vertices.Length + endPointLength != 3)
type = PathType.BEZIER; 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. // 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 // 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. // 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; continue;
// The last control point of each segment is not allowed to start a new implicit segment. // 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="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="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
/// <returns>The hit object.</returns> /// <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> /// <summary>
/// Creats a legacy Slider-type hit object. /// 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="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> /// <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> /// <returns>The hit object.</returns>
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, private ConvertHitObject createSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
IList<IList<HitSampleInfo>> nodeSamples); 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> /// <summary>
/// Creates a legacy Spinner-type hit object. /// Creates a legacy Spinner-type hit object.
/// </summary> /// </summary>
/// <param name="position">The position of the hit object.</param> /// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</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> /// <param name="duration">The spinner duration.</param>
/// <returns>The hit object.</returns> /// <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> /// <summary>
/// Creates a legacy Hold-type hit object. /// Creates a legacy Hold-type hit object.
/// </summary> /// </summary>
/// <param name="position">The position of the hit object.</param> /// <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> /// <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) private List<HitSampleInfo> convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo)
{ {
@ -511,21 +550,19 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <summary> /// <summary>
/// An optional overriding filename which causes all bank/sample specifications to be ignored. /// An optional overriding filename which causes all bank/sample specifications to be ignored.
/// </summary> /// </summary>
public string Filename; public string? Filename;
/// <summary> /// <summary>
/// The bank identifier to use for the base ("hitnormal") sample. /// The bank identifier to use for the base ("hitnormal") sample.
/// Transferred to <see cref="HitSampleInfo.Bank"/> when appropriate. /// Transferred to <see cref="HitSampleInfo.Bank"/> when appropriate.
/// </summary> /// </summary>
[CanBeNull] public string? BankForNormal;
public string BankForNormal;
/// <summary> /// <summary>
/// The bank identifier to use for additions ("hitwhistle", "hitfinish", "hitclap"). /// The bank identifier to use for additions ("hitwhistle", "hitfinish", "hitclap").
/// Transferred to <see cref="HitSampleInfo.Bank"/> when appropriate. /// Transferred to <see cref="HitSampleInfo.Bank"/> when appropriate.
/// </summary> /// </summary>
[CanBeNull] public string? BankForAdditions;
public string BankForAdditions;
/// <summary> /// <summary>
/// Hit sample volume (0-100). /// Hit sample volume (0-100).
@ -548,8 +585,6 @@ namespace osu.Game.Rulesets.Objects.Legacy
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
} }
#nullable enable
public class LegacyHitSampleInfo : HitSampleInfo, IEquatable<LegacyHitSampleInfo> public class LegacyHitSampleInfo : HitSampleInfo, IEquatable<LegacyHitSampleInfo>
{ {
public readonly int CustomSampleBank; public readonly int CustomSampleBank;
@ -577,13 +612,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
IsLayered = isLayered; 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); => 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, public virtual LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default,
Optional<int> newCustomSampleBank = default, Optional<bool> newEditorAutoBank = default, Optional<int> newCustomSampleBank = default, Optional<bool> newIsLayered = default)
Optional<bool> newIsLayered = default) => new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank), newCustomSampleBank.GetOr(CustomSampleBank),
=> new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered)); newIsLayered.GetOr(IsLayered));
public bool Equals(LegacyHitSampleInfo? other) public bool Equals(LegacyHitSampleInfo? other)
// The additions to equality checks here are *required* to ensure that pooling works correctly. // 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) 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, public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default,
Optional<int> newCustomSampleBank = default, Optional<bool> newEditorAutoBank = default, Optional<int> newCustomSampleBank = default, Optional<bool> newIsLayered = default)
Optional<bool> newIsLayered = default)
=> new FileHitSampleInfo(Filename, newVolume.GetOr(Volume)); => new FileHitSampleInfo(Filename, newVolume.GetOr(Volume));
public bool Equals(FileHitSampleInfo? other) public bool Equals(FileHitSampleInfo? other)

View File

@ -3,17 +3,18 @@
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy.Catch namespace osu.Game.Rulesets.Objects.Legacy
{ {
/// <summary> /// <summary>
/// Legacy osu!catch Spinner-type, used for parsing Beatmaps. /// Legacy "Hold" hit object type. Generally only valid in the mania ruleset.
/// </summary> /// </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 double Duration { get; set; }
public float X => 256; // Required for CatchBeatmapConverter public double EndTime => StartTime + Duration;
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -13,7 +11,13 @@ using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Objects.Legacy 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> /// <summary>
/// Scoring distance with a speed-adjusted beat length of 1 second. /// 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; set => SliderVelocityMultiplierBindable.Value = value;
} }
public bool GenerateTicks { get; set; } = true;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

View File

@ -3,11 +3,14 @@
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy.Taiko namespace osu.Game.Rulesets.Objects.Legacy
{ {
/// <summary> /// <summary>
/// Legacy osu!taiko Spinner-type, used for parsing Beatmaps. /// Legacy "Spinner" hit object type.
/// </summary> /// </summary>
/// <remarks>
/// Only used for parsing beatmaps and not gameplay.
/// </remarks>
internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration
{ {
public double Duration { get; set; } public double Duration { get; set; }

View 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; }
}
}

View File

@ -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; }
}
}

View File

@ -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
};
}
}
}

View File

@ -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;
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
{
}
}

View File

@ -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;
}
}
}

View File

@ -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
{
}
}

View File

@ -119,6 +119,11 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
public long MaximumTotalScore { get; private set; } public long MaximumTotalScore { get; private set; }
/// <summary>
/// The maximum achievable combo.
/// </summary>
public int MaximumCombo { get; private set; }
/// <summary> /// <summary>
/// The maximum sum of accuracy-affecting judgements at the current point in time. /// The maximum sum of accuracy-affecting judgements at the current point in time.
/// </summary> /// </summary>
@ -423,6 +428,7 @@ namespace osu.Game.Rulesets.Scoring
MaximumResultCounts.AddRange(ScoreResultCounts); MaximumResultCounts.AddRange(ScoreResultCounts);
MaximumTotalScore = TotalScore.Value; MaximumTotalScore = TotalScore.Value;
MaximumCombo = HighestCombo.Value;
} }
ScoreResultCounts.Clear(); ScoreResultCounts.Clear();

View File

@ -17,6 +17,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
protected OsuSpriteText Label { get; private set; } protected OsuSpriteText Label { get; private set; }
protected Container LabelContainer { get; private set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
@ -26,7 +28,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new Container LabelContainer = new Container
{ {
AutoSizeAxes = Axes.X, AutoSizeAxes = Axes.X,
Height = 16, Height = 16,

View File

@ -16,6 +16,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
@ -40,6 +41,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved] [Resolved]
private Editor? editor { get; set; } private Editor? editor { get; set; }
[Resolved]
private TimelineBlueprintContainer? timelineBlueprintContainer { get; set; }
public SamplePointPiece(HitObject hitObject) public SamplePointPiece(HitObject hitObject)
{ {
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; protected virtual double GetTime() => HitObject is IHasRepeats r ? HitObject.StartTime + r.Duration / r.SpanCount() / 2 : HitObject.StartTime;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OsuConfigManager config)
{ {
HitObject.DefaultsApplied += _ => updateText(); HitObject.DefaultsApplied += _ => updateText();
Label.AllowMultiline = false;
LabelContainer.AutoSizeAxes = Axes.None;
updateText(); updateText();
if (editor != null) if (editor != null)
editor.ShowSampleEditPopoverRequested += onShowSampleEditPopoverRequested; 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) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
@ -87,6 +121,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateText() private void updateText()
{ {
Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}"; Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}";
if (!contracted.Value)
LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint);
} }
private static string? abbreviateBank(string? bank) private static string? abbreviateBank(string? bank)

View File

@ -19,17 +19,22 @@ using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
[Cached]
internal partial class TimelineBlueprintContainer : EditorBlueprintContainer internal partial class TimelineBlueprintContainer : EditorBlueprintContainer
{ {
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private Timeline timeline { get; set; } private Timeline timeline { get; set; }
[Resolved(CanBeNull = true)]
private EditorClock editorClock { get; set; }
private Bindable<HitObject> placement; private Bindable<HitObject> placement;
private SelectionBlueprint<HitObject> placementBlueprint; private SelectionBlueprint<HitObject> placementBlueprint;
@ -118,9 +123,53 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
base.Update(); base.Update();
updateSamplePointContractedState();
updateStacking(); 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 readonly Stack<HitObject> currentConcurrentObjects = new Stack<HitObject>();
private void updateStacking() private void updateStacking()
@ -291,6 +340,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
protected override HitObjectOrderedSelectionContainer Content { get; } protected override HitObjectOrderedSelectionContainer Content { get; }
public Vector2 ContentRelativeToAbsoluteFactor => Content.RelativeToAbsoluteFactor;
public TimelineSelectionBlueprintContainer() public TimelineSelectionBlueprintContainer()
{ {
AddInternal(new TimelinePart<SelectionBlueprint<HitObject>>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); AddInternal(new TimelinePart<SelectionBlueprint<HitObject>>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms; using osu.Framework.Graphics.Transforms;
@ -32,10 +33,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override Container<Drawable> Content => zoomedContent; protected override Container<Drawable> Content => zoomedContent;
/// <summary> /// <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. /// It may differ from <see cref="Zoom"/> during transitions.
/// </summary> /// </summary>
public float CurrentZoom { get; private set; } = 1; public BindableFloat CurrentZoom { get; private set; } = new BindableFloat(1);
private bool isZoomSetUp; private bool isZoomSetUp;
@ -98,7 +99,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
minZoom = minimum; minZoom = minimum;
maxZoom = maximum; maxZoom = maximum;
CurrentZoom = zoomTarget = initial; CurrentZoom.Value = zoomTarget = initial;
zoomedContentWidthCache.Invalidate(); zoomedContentWidthCache.Invalidate();
isZoomSetUp = true; isZoomSetUp = true;
@ -124,7 +125,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (IsLoaded) if (IsLoaded)
setZoomTarget(newZoom, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X); setZoomTarget(newZoom, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X);
else else
CurrentZoom = zoomTarget = newZoom; CurrentZoom.Value = zoomTarget = newZoom;
} }
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
@ -154,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateZoomedContentWidth() private void updateZoomedContentWidth()
{ {
zoomedContent.Width = DrawWidth * CurrentZoom; zoomedContent.Width = DrawWidth * CurrentZoom.Value;
zoomedContentWidthCache.Validate(); zoomedContentWidthCache.Validate();
} }
@ -238,7 +239,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
float expectedWidth = d.DrawWidth * newZoom; float expectedWidth = d.DrawWidth * newZoom;
float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset; float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset;
d.CurrentZoom = newZoom; d.CurrentZoom.Value = newZoom;
d.updateZoomedContentWidth(); d.updateZoomedContentWidth();
// Temporarily here to make sure ScrollTo gets the correct DrawSize for scrollable area. // 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); d.ScrollTo(targetOffset, false);
} }
protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.CurrentZoom; protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.CurrentZoom.Value;
} }
} }
} }

View File

@ -53,10 +53,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(); var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator();
// Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. // 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; 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)); Schedule(() => setPerformanceValue(score, result.Total));
}, cancellationToken ?? default); }, cancellationToken ?? default);

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -26,7 +27,6 @@ namespace osu.Game.Screens.Ranking.Statistics
public partial class PerformanceBreakdownChart : Container public partial class PerformanceBreakdownChart : Container
{ {
private readonly ScoreInfo score; private readonly ScoreInfo score;
private readonly IBeatmap playableBeatmap;
private Drawable spinner = null!; private Drawable spinner = null!;
private Drawable content = null!; private Drawable content = null!;
@ -42,7 +42,6 @@ namespace osu.Game.Screens.Ranking.Statistics
public PerformanceBreakdownChart(ScoreInfo score, IBeatmap playableBeatmap) public PerformanceBreakdownChart(ScoreInfo score, IBeatmap playableBeatmap)
{ {
this.score = score; this.score = score;
this.playableBeatmap = playableBeatmap;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -142,12 +141,33 @@ namespace osu.Game.Screens.Ranking.Statistics
spinner.Show(); spinner.Show();
new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache) computePerformance(cancellationTokenSource.Token)
.CalculateAsync(score, cancellationTokenSource.Token) .ContinueWith(t => Schedule(() =>
.ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()!))); {
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(); spinner.Hide();
content.FadeIn(200); content.FadeIn(200);
@ -236,6 +256,8 @@ namespace osu.Game.Screens.Ranking.Statistics
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
cancellationTokenSource.Cancel(); cancellationTokenSource.Cancel();
cancellationTokenSource.Dispose();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }
} }

View File

@ -177,6 +177,9 @@ namespace osu.Game.Skinning.Components
case BeatmapAttribute.BPM: case BeatmapAttribute.BPM:
return BeatmapsetsStrings.ShowStatsBpm; return BeatmapsetsStrings.ShowStatsBpm;
case BeatmapAttribute.MaxPP:
return BeatmapAttributeTextStrings.MaxPP;
default: default:
return string.Empty; return string.Empty;
} }
@ -225,6 +228,9 @@ namespace osu.Game.Skinning.Components
case BeatmapAttribute.StarRating: case BeatmapAttribute.StarRating:
return (starDifficulty?.Stars ?? 0).ToLocalisableString(@"F2"); return (starDifficulty?.Stars ?? 0).ToLocalisableString(@"F2");
case BeatmapAttribute.MaxPP:
return Math.Round(starDifficulty?.PerformanceAttributes?.Total ?? 0, MidpointRounding.AwayFromZero).ToLocalisableString();
default: default:
return string.Empty; return string.Empty;
} }
@ -279,5 +285,6 @@ namespace osu.Game.Skinning.Components
RankedStatus, RankedStatus,
BPM, BPM,
Source, Source,
MaxPP
} }
} }