1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-18 02:02:54 +08:00
osu-lazer/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs

641 lines
25 KiB
C#
Raw Normal View History

// 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.
2018-04-13 17:19:50 +08:00
using System;
2019-10-25 18:58:42 +08:00
using System.Collections.Generic;
2018-04-13 17:19:50 +08:00
using System.IO;
using System.Linq;
2019-11-19 20:34:35 +08:00
using osu.Framework.Extensions;
using osu.Framework.Logging;
2022-10-19 19:34:41 +08:00
using osu.Game.Audio;
2018-04-13 17:19:50 +08:00
using osu.Game.Beatmaps.ControlPoints;
2019-12-10 19:19:31 +08:00
using osu.Game.Beatmaps.Legacy;
using osu.Game.Beatmaps.Timing;
using osu.Game.IO;
using osu.Game.Rulesets;
2023-04-25 18:12:46 +08:00
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
2023-04-25 18:52:21 +08:00
using osu.Game.Rulesets.Objects.Types;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Beatmaps.Formats
{
public class LegacyBeatmapDecoder : LegacyDecoder<Beatmap>
{
/// <summary>
/// An offset which needs to be applied to old beatmaps (v4 and lower) to correct timing changes that were applied at a game client level.
/// </summary>
public const int EARLY_VERSION_TIMING_OFFSET = 24;
2023-04-25 18:52:21 +08:00
/// <summary>
/// A small adjustment to the start time of sample control points to account for rounding/precision errors.
2023-04-25 18:52:21 +08:00
/// </summary>
/// <remarks>
/// Compare: https://github.com/peppy/osu-stable-reference/blob/master/osu!/GameplayElements/HitObjects/HitObject.cs#L319
/// </remarks>
Fix slider tail volume not saving Closes https://github.com/ppy/osu/issues/28587. As outlined in the issue thread, the tail volume wasn't saving because it wasn't actually attached to a hitobject properly, and as such the `LegacyBeatmapEncoder` logic, which is based on hitobjects, did not pick them up on save. To fix that, switch to using `NodeSamples` for objects that are `IHasRepeats`. That has one added complication in that having it work properly requires changes to the decode side too. That is because the intent is to allow the user to change the sample settings for each node (which are specified via `NodeSamples`), as well as "the rest of the object", which generally means ticks or auxiliary samples like `sliderslide` (which are specified by `Samples`). However, up until now, `Samples` always queried the control point which was _active at the end time of the slider_. This obviously can't work anymore when converting `NodeSamples` to legacy control points, because the last node's sample is _also_ at the end time of the slider. To bypass that, add extra sample points after each node (just out of reach of the 5ms leniency), which are supposed to control volume of ticks and/or slides. Upon testing, this *sort of* has the intended effect in stable, with the exception of `sliderslide`, which seems to either respect or _not_ respect the relevant volume spec dependent on... not sure what, and not sure I want to be debugging that. It might be frame alignment, or it might be the phase of the moon.
2024-06-26 20:27:14 +08:00
public const double CONTROL_POINT_LENIENCY = 5;
2023-04-25 18:52:21 +08:00
internal static RulesetStore? RulesetStore;
private Beatmap beatmap = null!;
2018-04-13 17:19:50 +08:00
private ConvertHitObjectParser? parser;
2018-04-13 17:19:50 +08:00
private LegacySampleBank defaultSampleBank;
private int defaultSampleVolume = 100;
public static void Register()
{
AddDecoder<Beatmap>(@"osu file format v", m => new LegacyBeatmapDecoder(Parsing.ParseInt(m.Split('v').Last())));
SetFallbackDecoder<Beatmap>(() => new LegacyBeatmapDecoder());
2018-04-13 17:19:50 +08:00
}
/// <summary>
/// Whether or not beatmap or runtime offsets should be applied. Defaults on; only disable for testing purposes.
/// </summary>
public bool ApplyOffsets = true;
private readonly int offset;
2018-04-13 17:19:50 +08:00
public LegacyBeatmapDecoder(int version = LATEST_VERSION)
: base(version)
2018-04-13 17:19:50 +08:00
{
if (RulesetStore == null)
{
Logger.Log($"A {nameof(RulesetStore)} was not provided via {nameof(Decoder)}.{nameof(RegisterDependencies)}; falling back to default {nameof(AssemblyRulesetStore)}.");
RulesetStore = new AssemblyRulesetStore();
}
offset = FormatVersion < 5 ? EARLY_VERSION_TIMING_OFFSET : 0;
2018-04-13 17:19:50 +08:00
}
protected override Beatmap CreateTemplateObject()
{
var templateBeatmap = base.CreateTemplateObject();
templateBeatmap.ControlPointInfo = new LegacyControlPointInfo();
return templateBeatmap;
}
protected override void ParseStreamInto(LineBufferedReader stream, Beatmap beatmap)
2018-04-13 17:19:50 +08:00
{
this.beatmap = beatmap;
this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion;
applyLegacyDefaults(this.beatmap.BeatmapInfo);
2018-04-13 17:19:50 +08:00
base.ParseStreamInto(stream, beatmap);
2024-05-22 23:25:59 +08:00
applyDifficultyRestrictions(beatmap.Difficulty, beatmap);
2019-10-25 18:58:42 +08:00
flushPendingPoints();
2018-05-16 12:59:51 +08:00
// Objects may be out of order *only* if a user has manually edited an .osu file.
// Unfortunately there are ranked maps in this state (example: https://osu.ppy.sh/s/594828).
// OrderBy is used to guarantee that the parsing order of hitobjects with equal start times is maintained (stably-sorted)
// The parsing order of hitobjects matters in mania difficulty calculation
2018-05-16 12:30:48 +08:00
this.beatmap.HitObjects = this.beatmap.HitObjects.OrderBy(h => h.StartTime).ToList();
2018-04-13 17:19:50 +08:00
postProcessBreaks(this.beatmap);
2018-04-13 17:19:50 +08:00
foreach (var hitObject in this.beatmap.HitObjects)
{
applyDefaults(hitObject);
applySamples(hitObject);
}
2018-04-13 17:19:50 +08:00
}
/// <summary>
2024-05-22 21:52:57 +08:00
/// Ensures that all <see cref="BeatmapDifficulty"/> settings are within the allowed ranges.
/// See also: https://github.com/peppy/osu-stable-reference/blob/0e425c0d525ef21353c8293c235cc0621d28338b/osu!/GameplayElements/Beatmaps/Beatmap.cs#L567-L614
/// </summary>
2024-05-22 23:25:59 +08:00
private static void applyDifficultyRestrictions(BeatmapDifficulty difficulty, Beatmap beatmap)
{
difficulty.DrainRate = Math.Clamp(difficulty.DrainRate, 0, 10);
2024-05-22 21:52:57 +08:00
// mania uses "circle size" for key count, thus different allowable range
difficulty.CircleSize = beatmap.BeatmapInfo.Ruleset.OnlineID != 3
? Math.Clamp(difficulty.CircleSize, 0, 10)
: Math.Clamp(difficulty.CircleSize, 1, 18);
difficulty.OverallDifficulty = Math.Clamp(difficulty.OverallDifficulty, 0, 10);
difficulty.ApproachRate = Math.Clamp(difficulty.ApproachRate, 0, 10);
difficulty.SliderMultiplier = Math.Clamp(difficulty.SliderMultiplier, 0.4, 3.6);
difficulty.SliderTickRate = Math.Clamp(difficulty.SliderTickRate, 0.5, 8);
}
/// <summary>
/// Processes the beatmap such that a new combo is started the first hitobject following each break.
/// </summary>
2024-05-22 23:25:59 +08:00
private static void postProcessBreaks(Beatmap beatmap)
{
int currentBreak = 0;
bool forceNewCombo = false;
foreach (var h in beatmap.HitObjects.OfType<ConvertHitObject>())
{
while (currentBreak < beatmap.Breaks.Count && beatmap.Breaks[currentBreak].EndTime < h.StartTime)
{
forceNewCombo = true;
currentBreak++;
}
h.NewCombo |= forceNewCombo;
forceNewCombo = false;
}
}
private void applyDefaults(HitObject hitObject)
2023-04-25 18:12:46 +08:00
{
DifficultyControlPoint difficultyControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.DifficultyPointAt(hitObject.StartTime) ?? DifficultyControlPoint.DEFAULT;
if (hitObject is IHasGenerateTicks hasGenerateTicks)
hasGenerateTicks.GenerateTicks = difficultyControlPoint.GenerateTicks;
2023-04-25 18:12:46 +08:00
2023-04-25 18:52:21 +08:00
if (hitObject is IHasSliderVelocity hasSliderVelocity)
hasSliderVelocity.SliderVelocityMultiplier = difficultyControlPoint.SliderVelocity;
2023-04-25 18:52:21 +08:00
hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
}
private void applySamples(HitObject hitObject)
{
if (hitObject is IHasRepeats hasRepeats)
2023-04-25 18:52:21 +08:00
{
Fix slider tail volume not saving Closes https://github.com/ppy/osu/issues/28587. As outlined in the issue thread, the tail volume wasn't saving because it wasn't actually attached to a hitobject properly, and as such the `LegacyBeatmapEncoder` logic, which is based on hitobjects, did not pick them up on save. To fix that, switch to using `NodeSamples` for objects that are `IHasRepeats`. That has one added complication in that having it work properly requires changes to the decode side too. That is because the intent is to allow the user to change the sample settings for each node (which are specified via `NodeSamples`), as well as "the rest of the object", which generally means ticks or auxiliary samples like `sliderslide` (which are specified by `Samples`). However, up until now, `Samples` always queried the control point which was _active at the end time of the slider_. This obviously can't work anymore when converting `NodeSamples` to legacy control points, because the last node's sample is _also_ at the end time of the slider. To bypass that, add extra sample points after each node (just out of reach of the 5ms leniency), which are supposed to control volume of ticks and/or slides. Upon testing, this *sort of* has the intended effect in stable, with the exception of `sliderslide`, which seems to either respect or _not_ respect the relevant volume spec dependent on... not sure what, and not sure I want to be debugging that. It might be frame alignment, or it might be the phase of the moon.
2024-06-26 20:27:14 +08:00
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1) ?? SampleControlPoint.DEFAULT;
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
{
Fix slider tail volume not saving Closes https://github.com/ppy/osu/issues/28587. As outlined in the issue thread, the tail volume wasn't saving because it wasn't actually attached to a hitobject properly, and as such the `LegacyBeatmapEncoder` logic, which is based on hitobjects, did not pick them up on save. To fix that, switch to using `NodeSamples` for objects that are `IHasRepeats`. That has one added complication in that having it work properly requires changes to the decode side too. That is because the intent is to allow the user to change the sample settings for each node (which are specified via `NodeSamples`), as well as "the rest of the object", which generally means ticks or auxiliary samples like `sliderslide` (which are specified by `Samples`). However, up until now, `Samples` always queried the control point which was _active at the end time of the slider_. This obviously can't work anymore when converting `NodeSamples` to legacy control points, because the last node's sample is _also_ at the end time of the slider. To bypass that, add extra sample points after each node (just out of reach of the 5ms leniency), which are supposed to control volume of ticks and/or slides. Upon testing, this *sort of* has the intended effect in stable, with the exception of `sliderslide`, which seems to either respect or _not_ respect the relevant volume spec dependent on... not sure what, and not sure I want to be debugging that. It might be frame alignment, or it might be the phase of the moon.
2024-06-26 20:27:14 +08:00
double time = hitObject.StartTime + i * hasRepeats.Duration / hasRepeats.SpanCount() + CONTROL_POINT_LENIENCY;
var nodeSamplePoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(time) ?? SampleControlPoint.DEFAULT;
2023-04-25 18:52:21 +08:00
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(o => nodeSamplePoint.ApplyTo(o)).ToList();
}
2023-04-25 18:52:21 +08:00
}
Fix slider tail volume not saving Closes https://github.com/ppy/osu/issues/28587. As outlined in the issue thread, the tail volume wasn't saving because it wasn't actually attached to a hitobject properly, and as such the `LegacyBeatmapEncoder` logic, which is based on hitobjects, did not pick them up on save. To fix that, switch to using `NodeSamples` for objects that are `IHasRepeats`. That has one added complication in that having it work properly requires changes to the decode side too. That is because the intent is to allow the user to change the sample settings for each node (which are specified via `NodeSamples`), as well as "the rest of the object", which generally means ticks or auxiliary samples like `sliderslide` (which are specified by `Samples`). However, up until now, `Samples` always queried the control point which was _active at the end time of the slider_. This obviously can't work anymore when converting `NodeSamples` to legacy control points, because the last node's sample is _also_ at the end time of the slider. To bypass that, add extra sample points after each node (just out of reach of the 5ms leniency), which are supposed to control volume of ticks and/or slides. Upon testing, this *sort of* has the intended effect in stable, with the exception of `sliderslide`, which seems to either respect or _not_ respect the relevant volume spec dependent on... not sure what, and not sure I want to be debugging that. It might be frame alignment, or it might be the phase of the moon.
2024-06-26 20:27:14 +08:00
else
{
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY) ?? SampleControlPoint.DEFAULT;
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
}
2018-04-13 17:19:50 +08:00
}
/// <summary>
/// Some `BeatmapInfo` members have default values that differ from the default values used by stable.
/// In addition, legacy beatmaps will sometimes not contain some configuration keys, in which case
/// the legacy default values should be used.
/// This method's intention is to restore those legacy defaults.
/// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29
/// </summary>
2024-05-22 23:25:59 +08:00
private static void applyLegacyDefaults(BeatmapInfo beatmapInfo)
{
beatmapInfo.WidescreenStoryboard = false;
beatmapInfo.SamplesMatchPlaybackRate = false;
}
2018-04-13 17:19:50 +08:00
protected override void ParseLine(Beatmap beatmap, Section section, string line)
{
switch (section)
{
case Section.General:
handleGeneral(line);
2018-04-13 17:19:50 +08:00
return;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case Section.Editor:
handleEditor(line);
2018-04-13 17:19:50 +08:00
return;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case Section.Metadata:
handleMetadata(line);
return;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case Section.Difficulty:
handleDifficulty(line);
2018-04-13 17:19:50 +08:00
return;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case Section.Events:
handleEvent(line);
2018-04-13 17:19:50 +08:00
return;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case Section.TimingPoints:
handleTimingPoint(line);
2018-04-13 17:19:50 +08:00
return;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case Section.HitObjects:
handleHitObject(line);
2018-04-13 17:19:50 +08:00
return;
}
base.ParseLine(beatmap, section, line);
}
private void handleGeneral(string line)
{
var pair = SplitKeyVal(line);
var metadata = beatmap.BeatmapInfo.Metadata;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
switch (pair.Key)
{
case @"AudioFilename":
2019-12-11 16:06:56 +08:00
metadata.AudioFile = pair.Value.ToStandardisedPath();
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"AudioLeadIn":
beatmap.BeatmapInfo.AudioLeadIn = Parsing.ParseInt(pair.Value);
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"PreviewTime":
int time = Parsing.ParseInt(pair.Value);
metadata.PreviewTime = time == -1 ? time : getOffsetTime(time);
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"SampleSet":
2022-12-27 03:36:39 +08:00
defaultSampleBank = Enum.Parse<LegacySampleBank>(pair.Value);
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"SampleVolume":
defaultSampleVolume = Parsing.ParseInt(pair.Value);
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"StackLeniency":
beatmap.BeatmapInfo.StackLeniency = Parsing.ParseFloat(pair.Value);
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"Mode":
int rulesetID = Parsing.ParseInt(pair.Value);
2018-04-13 17:19:50 +08:00
beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally.");
switch (rulesetID)
2018-04-13 17:19:50 +08:00
{
case 0:
parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case 1:
parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case 2:
parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case 3:
parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
2018-04-13 17:19:50 +08:00
break;
}
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"LetterboxInBreaks":
beatmap.BeatmapInfo.LetterboxInBreaks = Parsing.ParseInt(pair.Value) == 1;
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"SpecialStyle":
beatmap.BeatmapInfo.SpecialStyle = Parsing.ParseInt(pair.Value) == 1;
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"WidescreenStoryboard":
beatmap.BeatmapInfo.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1;
2018-04-13 17:19:50 +08:00
break;
2020-10-20 05:53:41 +08:00
2020-07-20 18:36:42 +08:00
case @"EpilepsyWarning":
beatmap.BeatmapInfo.EpilepsyWarning = Parsing.ParseInt(pair.Value) == 1;
break;
case @"SamplesMatchPlaybackRate":
beatmap.BeatmapInfo.SamplesMatchPlaybackRate = Parsing.ParseInt(pair.Value) == 1;
break;
case @"Countdown":
2022-12-27 03:38:35 +08:00
beatmap.BeatmapInfo.Countdown = Enum.Parse<CountdownType>(pair.Value);
break;
case @"CountdownOffset":
beatmap.BeatmapInfo.CountdownOffset = Parsing.ParseInt(pair.Value);
break;
2018-04-13 17:19:50 +08:00
}
}
private void handleEditor(string line)
{
var pair = SplitKeyVal(line);
switch (pair.Key)
{
case @"Bookmarks":
beatmap.BeatmapInfo.Bookmarks = pair.Value.Split(',').Select(v =>
{
bool result = int.TryParse(v, out int val);
return new { result, val };
}).Where(p => p.result).Select(p => p.val).ToArray();
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"DistanceSpacing":
beatmap.BeatmapInfo.DistanceSpacing = Math.Max(0, Parsing.ParseDouble(pair.Value));
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"BeatDivisor":
beatmap.BeatmapInfo.BeatDivisor = Parsing.ParseInt(pair.Value);
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"GridSize":
beatmap.BeatmapInfo.GridSize = Parsing.ParseInt(pair.Value);
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"TimelineZoom":
beatmap.BeatmapInfo.TimelineZoom = Math.Max(0, Parsing.ParseDouble(pair.Value));
2018-04-13 17:19:50 +08:00
break;
}
}
private void handleMetadata(string line)
{
var pair = SplitKeyVal(line);
var metadata = beatmap.BeatmapInfo.Metadata;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
switch (pair.Key)
{
case @"Title":
metadata.Title = pair.Value;
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"TitleUnicode":
metadata.TitleUnicode = pair.Value;
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"Artist":
metadata.Artist = pair.Value;
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"ArtistUnicode":
metadata.ArtistUnicode = pair.Value;
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"Creator":
2022-01-18 22:30:40 +08:00
metadata.Author.Username = pair.Value;
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"Version":
beatmap.BeatmapInfo.DifficultyName = pair.Value;
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"Source":
metadata.Source = pair.Value;
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"Tags":
metadata.Tags = pair.Value;
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"BeatmapID":
beatmap.BeatmapInfo.OnlineID = Parsing.ParseInt(pair.Value);
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"BeatmapSetID":
beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineID = Parsing.ParseInt(pair.Value) };
2018-04-13 17:19:50 +08:00
break;
}
}
private void handleDifficulty(string line)
{
var pair = SplitKeyVal(line);
var difficulty = beatmap.Difficulty;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
switch (pair.Key)
{
case @"HPDrainRate":
difficulty.DrainRate = Parsing.ParseFloat(pair.Value);
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"CircleSize":
difficulty.CircleSize = Parsing.ParseFloat(pair.Value);
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"OverallDifficulty":
difficulty.OverallDifficulty = Parsing.ParseFloat(pair.Value);
if (!hasApproachRate)
difficulty.ApproachRate = difficulty.OverallDifficulty;
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"ApproachRate":
difficulty.ApproachRate = Parsing.ParseFloat(pair.Value);
hasApproachRate = true;
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"SliderMultiplier":
difficulty.SliderMultiplier = Parsing.ParseDouble(pair.Value);
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"SliderTickRate":
difficulty.SliderTickRate = Parsing.ParseDouble(pair.Value);
2018-04-13 17:19:50 +08:00
break;
}
}
private void handleEvent(string line)
{
string[] split = line.Split(',');
// Until we have full storyboard encoder coverage, let's track any lines which aren't handled
// and store them to a temporary location such that they aren't lost on editor save / export.
bool lineSupportedByEncoder = false;
if (Enum.TryParse(split[0], out LegacyEventType type))
2018-04-13 17:19:50 +08:00
{
switch (type)
{
case LegacyEventType.Sprite:
// Generally, the background is the first thing defined in a beatmap file.
// In some older beatmaps, it is not present and replaced by a storyboard-level background instead.
// Allow the first sprite (by file order) to act as the background in such cases.
if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
{
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
lineSupportedByEncoder = true;
}
break;
case LegacyEventType.Video:
string filename = CleanFilename(split[2]);
// Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO
// instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported
// video extensions and handle similar to a background if it doesn't match.
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()))
{
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
lineSupportedByEncoder = true;
}
break;
case LegacyEventType.Background:
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
lineSupportedByEncoder = true;
break;
case LegacyEventType.Break:
double start = getOffsetTime(Parsing.ParseDouble(split[1]));
double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])));
beatmap.Breaks.Add(new BreakPeriod(start, end));
lineSupportedByEncoder = true;
break;
}
2018-04-13 17:19:50 +08:00
}
if (!lineSupportedByEncoder)
beatmap.UnhandledEventLines.Add(line);
2018-04-13 17:19:50 +08:00
}
private void handleTimingPoint(string line)
{
string[] split = line.Split(',');
double time = getOffsetTime(Parsing.ParseDouble(split[0].Trim()));
2022-08-24 14:10:19 +08:00
// beatLength is allowed to be NaN to handle an edge case in which some beatmaps use NaN slider velocity to disable slider tick generation (see LegacyDifficultyControlPoint).
double beatLength = Parsing.ParseDouble(split[1].Trim(), allowNaN: true);
// If beatLength is NaN, speedMultiplier should still be 1 because all comparisons against NaN are false.
double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1;
TimeSignature timeSignature = TimeSignature.SimpleQuadruple;
if (split.Length >= 3)
timeSignature = split[2][0] == '0' ? TimeSignature.SimpleQuadruple : new TimeSignature(Parsing.ParseInt(split[2]));
LegacySampleBank sampleSet = defaultSampleBank;
if (split.Length >= 4)
sampleSet = (LegacySampleBank)Parsing.ParseInt(split[3]);
int customSampleBank = 0;
if (split.Length >= 5)
customSampleBank = Parsing.ParseInt(split[4]);
int sampleVolume = defaultSampleVolume;
if (split.Length >= 6)
sampleVolume = Parsing.ParseInt(split[5]);
bool timingChange = true;
if (split.Length >= 7)
timingChange = split[6][0] == '1';
bool kiaiMode = false;
bool omitFirstBarSignature = false;
if (split.Length >= 8)
2018-04-13 17:19:50 +08:00
{
2019-12-10 19:19:31 +08:00
LegacyEffectFlags effectFlags = (LegacyEffectFlags)Parsing.ParseInt(split[7]);
2024-07-02 23:19:04 +08:00
kiaiMode = effectFlags.HasFlag(LegacyEffectFlags.Kiai);
omitFirstBarSignature = effectFlags.HasFlag(LegacyEffectFlags.OmitFirstBarLine);
2018-04-13 17:19:50 +08:00
}
string stringSampleSet = sampleSet.ToString().ToLowerInvariant();
if (stringSampleSet == @"none")
stringSampleSet = HitSampleInfo.BANK_NORMAL;
if (timingChange)
2019-03-13 10:30:33 +08:00
{
if (double.IsNaN(beatLength))
throw new InvalidDataException("Beat length cannot be NaN in a timing control point");
var controlPoint = CreateTimingControlPoint();
2019-10-25 18:58:42 +08:00
controlPoint.BeatLength = beatLength;
controlPoint.TimeSignature = timeSignature;
controlPoint.OmitFirstBarLine = omitFirstBarSignature;
2019-10-25 18:58:42 +08:00
addControlPoint(time, controlPoint, true);
2019-03-13 10:30:33 +08:00
}
2019-10-25 18:58:42 +08:00
int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
addControlPoint(time, new DifficultyControlPoint
{
GenerateTicks = !double.IsNaN(beatLength),
SliderVelocity = speedMultiplier,
}, timingChange);
var effectPoint = new EffectControlPoint
{
KiaiMode = kiaiMode,
};
// osu!taiko and osu!mania use effect points rather than difficulty points for scroll speed adjustments.
if (onlineRulesetID == 1 || onlineRulesetID == 3)
effectPoint.ScrollSpeed = speedMultiplier;
addControlPoint(time, effectPoint, timingChange);
2019-10-25 18:58:42 +08:00
addControlPoint(time, new LegacySampleControlPoint
{
SampleBank = stringSampleSet,
SampleVolume = sampleVolume,
CustomSampleBank = customSampleBank,
2019-10-25 18:58:42 +08:00
}, timingChange);
}
private readonly List<ControlPoint> pendingControlPoints = new List<ControlPoint>();
private readonly HashSet<Type> pendingControlPointTypes = new HashSet<Type>();
2019-10-25 18:58:42 +08:00
private double pendingControlPointsTime;
private bool hasApproachRate;
2019-10-25 18:58:42 +08:00
private void addControlPoint(double time, ControlPoint point, bool timingChange)
{
if (time != pendingControlPointsTime)
flushPendingPoints();
2019-10-25 18:58:42 +08:00
if (timingChange)
pendingControlPoints.Insert(0, point);
else
pendingControlPoints.Add(point);
2019-10-25 18:58:42 +08:00
pendingControlPointsTime = time;
}
private void flushPendingPoints()
{
// Changes from non-timing-points are added to the end of the list (see addControlPoint()) and should override any changes from timing-points (added to the start of the list).
for (int i = pendingControlPoints.Count - 1; i >= 0; i--)
{
var type = pendingControlPoints[i].GetType();
2024-02-02 18:48:13 +08:00
if (!pendingControlPointTypes.Add(type))
continue;
beatmap.ControlPointInfo.Add(pendingControlPointsTime, pendingControlPoints[i]);
}
2019-10-25 18:58:42 +08:00
pendingControlPoints.Clear();
pendingControlPointTypes.Clear();
2018-04-13 17:19:50 +08:00
}
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);
2018-04-13 17:19:50 +08:00
var obj = parser.Parse(line);
2018-04-13 17:19:50 +08:00
if (obj != null)
{
obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
2018-04-13 17:19:50 +08:00
beatmap.HitObjects.Add(obj);
}
2018-04-13 17:19:50 +08:00
}
private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0);
private double getOffsetTime() => ApplyOffsets ? offset : 0;
2018-04-13 17:19:50 +08:00
private double getOffsetTime(double time) => time + (ApplyOffsets ? offset : 0);
2018-07-16 15:26:37 +08:00
protected virtual TimingControlPoint CreateTimingControlPoint() => new TimingControlPoint();
2018-04-13 17:19:50 +08:00
}
}