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:
commit
53fcb494ff
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Legacy osu!catch Hit-type, used for parsing Beatmaps.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class ConvertHit : ConvertHitObject, IHasPosition
|
|
||||||
{
|
|
||||||
public float X => Position.X;
|
|
||||||
|
|
||||||
public float Y => Position.Y;
|
|
||||||
|
|
||||||
public Vector2 Position { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osuTK;
|
|
||||||
using osu.Game.Audio;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A HitObjectParser to parse legacy osu!catch Beatmaps.
|
|
||||||
/// </summary>
|
|
||||||
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
|
|
||||||
{
|
|
||||||
private ConvertHitObject lastObject;
|
|
||||||
|
|
||||||
public ConvertHitObjectParser(double offset, int formatVersion)
|
|
||||||
: base(offset, formatVersion)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
|
|
||||||
{
|
|
||||||
return lastObject = new ConvertHit
|
|
||||||
{
|
|
||||||
Position = position,
|
|
||||||
NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo,
|
|
||||||
ComboOffset = newCombo ? comboOffset : 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
|
|
||||||
IList<IList<HitSampleInfo>> nodeSamples)
|
|
||||||
{
|
|
||||||
return lastObject = new ConvertSlider
|
|
||||||
{
|
|
||||||
Position = position,
|
|
||||||
NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo,
|
|
||||||
ComboOffset = newCombo ? comboOffset : 0,
|
|
||||||
Path = new SliderPath(controlPoints, length),
|
|
||||||
NodeSamples = nodeSamples,
|
|
||||||
RepeatCount = repeatCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
|
|
||||||
{
|
|
||||||
return lastObject = new ConvertSpinner
|
|
||||||
{
|
|
||||||
Duration = duration,
|
|
||||||
NewCombo = newCombo
|
|
||||||
// Spinners cannot have combo offset.
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
|
|
||||||
{
|
|
||||||
return lastObject = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Legacy osu!catch Slider-type, used for parsing Beatmaps.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition
|
|
||||||
{
|
|
||||||
public float X => Position.X;
|
|
||||||
|
|
||||||
public float Y => Position.Y;
|
|
||||||
|
|
||||||
public Vector2 Position { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
13
osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs
Normal file
13
osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Objects.Legacy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Legacy "HitCircle" hit object type.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Only used for parsing beatmaps and not gameplay.
|
||||||
|
/// </remarks>
|
||||||
|
internal sealed class ConvertHitCircle : ConvertHitObject;
|
||||||
|
}
|
@ -1,21 +1,34 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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;
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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);
|
||||||
|
@ -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; }
|
18
osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs
Normal file
18
osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Beatmaps.Legacy;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Objects.Legacy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A hit object from a legacy beatmap representation.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHasLegacyHitObjectType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The hit object type.
|
||||||
|
/// </summary>
|
||||||
|
LegacyHitObjectType LegacyType { get; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Mania
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Legacy osu!mania Hit-type, used for parsing Beatmaps.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class ConvertHit : ConvertHitObject, IHasXPosition
|
|
||||||
{
|
|
||||||
public float X { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osuTK;
|
|
||||||
using osu.Game.Audio;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Mania
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A HitObjectParser to parse legacy osu!mania Beatmaps.
|
|
||||||
/// </summary>
|
|
||||||
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
|
|
||||||
{
|
|
||||||
public ConvertHitObjectParser(double offset, int formatVersion)
|
|
||||||
: base(offset, formatVersion)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
|
|
||||||
{
|
|
||||||
return new ConvertHit
|
|
||||||
{
|
|
||||||
X = position.X
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
|
|
||||||
IList<IList<HitSampleInfo>> nodeSamples)
|
|
||||||
{
|
|
||||||
return new ConvertSlider
|
|
||||||
{
|
|
||||||
X = position.X,
|
|
||||||
Path = new SliderPath(controlPoints, length),
|
|
||||||
NodeSamples = nodeSamples,
|
|
||||||
RepeatCount = repeatCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
|
|
||||||
{
|
|
||||||
return new ConvertSpinner
|
|
||||||
{
|
|
||||||
X = position.X,
|
|
||||||
Duration = duration
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
|
|
||||||
{
|
|
||||||
return new ConvertHold
|
|
||||||
{
|
|
||||||
X = position.X,
|
|
||||||
Duration = duration
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Mania
|
|
||||||
{
|
|
||||||
internal sealed class ConvertHold : ConvertHitObject, IHasXPosition, IHasDuration
|
|
||||||
{
|
|
||||||
public float X { get; set; }
|
|
||||||
|
|
||||||
public double Duration { get; set; }
|
|
||||||
|
|
||||||
public double EndTime => StartTime + Duration;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Mania
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Legacy osu!mania Slider-type, used for parsing Beatmaps.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition
|
|
||||||
{
|
|
||||||
public float X { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Mania
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Legacy osu!mania Spinner-type, used for parsing Beatmaps.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition
|
|
||||||
{
|
|
||||||
public double Duration { get; set; }
|
|
||||||
|
|
||||||
public double EndTime => StartTime + Duration;
|
|
||||||
|
|
||||||
public float X { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Legacy osu! Hit-type, used for parsing Beatmaps.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class ConvertHit : ConvertHitObject, IHasPosition
|
|
||||||
{
|
|
||||||
public Vector2 Position { get; set; }
|
|
||||||
|
|
||||||
public float X => Position.X;
|
|
||||||
|
|
||||||
public float Y => Position.Y;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osuTK;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using osu.Game.Audio;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A HitObjectParser to parse legacy osu! Beatmaps.
|
|
||||||
/// </summary>
|
|
||||||
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
|
|
||||||
{
|
|
||||||
private ConvertHitObject lastObject;
|
|
||||||
|
|
||||||
public ConvertHitObjectParser(double offset, int formatVersion)
|
|
||||||
: base(offset, formatVersion)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
|
|
||||||
{
|
|
||||||
return lastObject = new ConvertHit
|
|
||||||
{
|
|
||||||
Position = position,
|
|
||||||
NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo,
|
|
||||||
ComboOffset = newCombo ? comboOffset : 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
|
|
||||||
IList<IList<HitSampleInfo>> nodeSamples)
|
|
||||||
{
|
|
||||||
return lastObject = new ConvertSlider
|
|
||||||
{
|
|
||||||
Position = position,
|
|
||||||
NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo,
|
|
||||||
ComboOffset = newCombo ? comboOffset : 0,
|
|
||||||
Path = new SliderPath(controlPoints, length),
|
|
||||||
NodeSamples = nodeSamples,
|
|
||||||
RepeatCount = repeatCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
|
|
||||||
{
|
|
||||||
return lastObject = new ConvertSpinner
|
|
||||||
{
|
|
||||||
Position = position,
|
|
||||||
Duration = duration,
|
|
||||||
NewCombo = newCombo
|
|
||||||
// Spinners cannot have combo offset.
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
|
|
||||||
{
|
|
||||||
return lastObject = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Legacy osu! Slider-type, used for parsing Beatmaps.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasGenerateTicks
|
|
||||||
{
|
|
||||||
public Vector2 Position { get; set; }
|
|
||||||
|
|
||||||
public float X => Position.X;
|
|
||||||
|
|
||||||
public float Y => Position.Y;
|
|
||||||
|
|
||||||
public bool GenerateTicks { get; set; } = true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Legacy osu! Spinner-type, used for parsing Beatmaps.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasPosition
|
|
||||||
{
|
|
||||||
public double Duration { get; set; }
|
|
||||||
|
|
||||||
public double EndTime => StartTime + Duration;
|
|
||||||
|
|
||||||
public Vector2 Position { get; set; }
|
|
||||||
|
|
||||||
public float X => Position.X;
|
|
||||||
|
|
||||||
public float Y => Position.Y;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Legacy osu!taiko Hit-type, used for parsing Beatmaps.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class ConvertHit : ConvertHitObject
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osuTK;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using osu.Game.Audio;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A HitObjectParser to parse legacy osu!taiko Beatmaps.
|
|
||||||
/// </summary>
|
|
||||||
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
|
|
||||||
{
|
|
||||||
public ConvertHitObjectParser(double offset, int formatVersion)
|
|
||||||
: base(offset, formatVersion)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
|
|
||||||
{
|
|
||||||
return new ConvertHit();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
|
|
||||||
IList<IList<HitSampleInfo>> nodeSamples)
|
|
||||||
{
|
|
||||||
return new ConvertSlider
|
|
||||||
{
|
|
||||||
Path = new SliderPath(controlPoints, length),
|
|
||||||
NodeSamples = nodeSamples,
|
|
||||||
RepeatCount = repeatCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
|
|
||||||
{
|
|
||||||
return new ConvertSpinner
|
|
||||||
{
|
|
||||||
Duration = duration
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Legacy osu!taiko Slider-type, used for parsing Beatmaps.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class ConvertSlider : Legacy.ConvertSlider
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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 });
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user