2019-01-24 16:43:03 +08:00
// 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
2023-05-03 12:19:13 +08:00
#pragma warning disable 618
2017-12-02 00:43:33 +08:00
using System ;
2019-10-25 18:58:42 +08:00
using System.Collections.Generic ;
2017-12-02 00:43:33 +08:00
using System.IO ;
2018-03-09 20:23:03 +08:00
using System.Linq ;
2019-11-19 20:34:35 +08:00
using osu.Framework.Extensions ;
2021-02-25 14:38:56 +08:00
using osu.Framework.Extensions.EnumExtensions ;
2022-02-18 15:48:30 +08:00
using osu.Framework.Logging ;
2022-10-19 19:34:41 +08:00
using osu.Game.Audio ;
2017-12-02 00:43:33 +08:00
using osu.Game.Beatmaps.ControlPoints ;
2019-12-10 19:19:31 +08:00
using osu.Game.Beatmaps.Legacy ;
2020-03-08 06:08:13 +08:00
using osu.Game.Beatmaps.Timing ;
using osu.Game.IO ;
2022-02-16 16:12:57 +08:00
using osu.Game.Rulesets ;
2023-04-25 18:12:46 +08:00
using osu.Game.Rulesets.Objects ;
2020-03-08 06:08:13 +08:00
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
2017-12-02 00:43:33 +08:00
namespace osu.Game.Beatmaps.Formats
{
2018-03-09 20:23:03 +08:00
public class LegacyBeatmapDecoder : LegacyDecoder < Beatmap >
2017-12-02 00:43:33 +08:00
{
2022-03-24 15:43:41 +08:00
/// <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 control points to account for rounding/precision errors.
/// </summary>
private const double control_point_leniency = 1 ;
2023-08-16 18:34:37 +08:00
internal static RulesetStore ? RulesetStore ;
2022-02-16 16:12:57 +08:00
2023-08-16 18:34:37 +08:00
private Beatmap beatmap = null ! ;
2018-04-13 17:19:50 +08:00
2023-08-16 18:34:37 +08:00
private ConvertHitObjectParser ? parser ;
2018-04-13 17:19:50 +08:00
2017-12-02 00:43:33 +08:00
private LegacySampleBank defaultSampleBank ;
private int defaultSampleVolume = 100 ;
2018-04-13 17:19:50 +08:00
2018-03-09 20:23:03 +08:00
public static void Register ( )
{
2019-03-13 12:56:31 +08:00
AddDecoder < Beatmap > ( @"osu file format v" , m = > new LegacyBeatmapDecoder ( Parsing . ParseInt ( m . Split ( 'v' ) . Last ( ) ) ) ) ;
2019-09-11 04:06:10 +08:00
SetFallbackDecoder < Beatmap > ( ( ) = > new LegacyBeatmapDecoder ( ) ) ;
2018-03-09 20:23:03 +08:00
}
2018-04-13 17:19:50 +08:00
2018-03-04 21:13:43 +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 ;
2018-04-13 17:19:50 +08:00
2018-05-28 02:00:21 +08:00
private readonly int offset ;
2018-04-13 17:19:50 +08:00
2018-06-08 14:26:27 +08:00
public LegacyBeatmapDecoder ( int version = LATEST_VERSION )
: base ( version )
2017-12-02 02:11:52 +08:00
{
2022-02-16 16:12:57 +08:00
if ( RulesetStore = = null )
2022-02-18 15:48:30 +08:00
{
Logger . Log ( $"A {nameof(RulesetStore)} was not provided via {nameof(Decoder)}.{nameof(RegisterDependencies)}; falling back to default {nameof(AssemblyRulesetStore)}." ) ;
RulesetStore = new AssemblyRulesetStore ( ) ;
}
2022-02-16 16:12:57 +08:00
2022-03-24 15:43:41 +08:00
offset = FormatVersion < 5 ? EARLY_VERSION_TIMING_OFFSET : 0 ;
2017-12-02 02:11:52 +08:00
}
2018-04-13 17:19:50 +08:00
2021-08-25 17:00:57 +08:00
protected override Beatmap CreateTemplateObject ( )
{
var templateBeatmap = base . CreateTemplateObject ( ) ;
templateBeatmap . ControlPointInfo = new LegacyControlPointInfo ( ) ;
return templateBeatmap ;
}
2019-09-10 06:43:30 +08:00
protected override void ParseStreamInto ( LineBufferedReader stream , Beatmap beatmap )
2017-12-02 05:05:01 +08:00
{
this . beatmap = beatmap ;
2018-03-12 10:33:12 +08:00
this . beatmap . BeatmapInfo . BeatmapVersion = FormatVersion ;
2018-04-13 17:19:50 +08:00
2022-01-28 04:41:30 +08:00
applyLegacyDefaults ( this . beatmap . BeatmapInfo ) ;
2018-03-09 20:23:03 +08:00
base . ParseStreamInto ( stream , beatmap ) ;
2018-04-13 17:19:50 +08:00
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
2017-12-02 05:05:01 +08:00
foreach ( var hitObject in this . beatmap . HitObjects )
2023-04-25 17:34:09 +08:00
{
2023-05-03 12:30:45 +08:00
applyDefaults ( hitObject ) ;
applySamples ( hitObject ) ;
2023-04-25 17:34:09 +08:00
}
2017-12-02 05:05:01 +08:00
}
2018-04-13 17:19:50 +08:00
2023-05-03 12:30:45 +08:00
private void applyDefaults ( HitObject hitObject )
2023-04-25 18:12:46 +08:00
{
2023-05-03 12:30:45 +08:00
DifficultyControlPoint difficultyControlPoint = ( beatmap . ControlPointInfo as LegacyControlPointInfo ) ? . DifficultyPointAt ( hitObject . StartTime ) ? ? DifficultyControlPoint . DEFAULT ;
2023-05-03 12:19:13 +08:00
2023-04-25 18:12:46 +08:00
if ( difficultyControlPoint is LegacyDifficultyControlPoint legacyDifficultyControlPoint )
2023-04-26 19:10:57 +08:00
{
hitObject . LegacyBpmMultiplier = legacyDifficultyControlPoint . BpmMultiplier ;
if ( hitObject is IHasGenerateTicks hasGenerateTicks )
hasGenerateTicks . GenerateTicks = legacyDifficultyControlPoint . GenerateTicks ;
}
2023-04-25 18:12:46 +08:00
2023-04-25 18:52:21 +08:00
if ( hitObject is IHasSliderVelocity hasSliderVelocity )
hasSliderVelocity . SliderVelocity = difficultyControlPoint . SliderVelocity ;
hitObject . ApplyDefaults ( beatmap . ControlPointInfo , beatmap . Difficulty ) ;
2023-05-03 12:30:45 +08:00
}
private void applySamples ( HitObject hitObject )
{
SampleControlPoint sampleControlPoint = ( beatmap . ControlPointInfo as LegacyControlPointInfo ) ? . SamplePointAt ( hitObject . GetEndTime ( ) + control_point_leniency ) ? ? SampleControlPoint . DEFAULT ;
2023-04-25 18:52:21 +08:00
2023-04-26 20:32:12 +08:00
hitObject . Samples = hitObject . Samples . Select ( o = > sampleControlPoint . ApplyTo ( o ) ) . ToList ( ) ;
2023-04-25 18:52:21 +08:00
2023-05-03 12:26:50 +08:00
if ( hitObject is IHasRepeats hasRepeats )
2023-04-25 18:52:21 +08:00
{
2023-05-03 12:26:50 +08:00
for ( int i = 0 ; i < hasRepeats . NodeSamples . Count ; i + + )
{
double time = hitObject . StartTime + i * hasRepeats . Duration / hasRepeats . SpanCount ( ) + control_point_leniency ;
2023-05-03 12:30:45 +08:00
var nodeSamplePoint = ( beatmap . ControlPointInfo as LegacyControlPointInfo ) ? . SamplePointAt ( time ) ? ? SampleControlPoint . DEFAULT ;
2023-04-25 18:52:21 +08:00
2023-05-03 12:30:45 +08:00
hasRepeats . NodeSamples [ i ] = hasRepeats . NodeSamples [ i ] . Select ( o = > nodeSamplePoint . ApplyTo ( o ) ) . ToList ( ) ;
2023-05-03 12:26:50 +08:00
}
2023-04-25 18:52:21 +08:00
}
2017-12-02 05:05:01 +08:00
}
2018-04-13 17:19:50 +08:00
2022-01-28 04:41:30 +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>
private void applyLegacyDefaults ( BeatmapInfo beatmapInfo )
{
beatmapInfo . WidescreenStoryboard = false ;
beatmapInfo . SamplesMatchPlaybackRate = false ;
}
2020-02-09 01:05:27 +08:00
protected override bool ShouldSkipLine ( string line ) = > base . ShouldSkipLine ( line ) | | line . StartsWith ( ' ' ) | | line . StartsWith ( '_' ) ;
2018-04-13 17:19:50 +08:00
2018-03-09 20:23:03 +08:00
protected override void ParseLine ( Beatmap beatmap , Section section , string line )
2017-12-02 00:43:33 +08:00
{
switch ( section )
{
case Section . General :
2021-03-18 15:30:30 +08:00
handleGeneral ( line ) ;
2018-03-13 18:13:50 +08:00
return ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case Section . Editor :
2021-03-18 15:30:30 +08:00
handleEditor ( line ) ;
2018-03-13 18:13:50 +08:00
return ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case Section . Metadata :
handleMetadata ( line ) ;
2018-03-13 18:13:50 +08:00
return ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case Section . Difficulty :
2021-03-18 15:30:30 +08:00
handleDifficulty ( line ) ;
2018-03-13 18:13:50 +08:00
return ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case Section . Events :
2021-03-18 15:30:30 +08:00
handleEvent ( line ) ;
2018-03-13 18:13:50 +08:00
return ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case Section . TimingPoints :
2021-03-18 15:30:30 +08:00
handleTimingPoint ( line ) ;
2018-03-13 18:13:50 +08:00
return ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case Section . HitObjects :
2021-03-18 15:30:30 +08:00
handleHitObject ( line ) ;
2018-03-13 18:13:50 +08:00
return ;
2017-12-02 00:43:33 +08:00
}
2018-04-13 17:19:50 +08:00
2018-03-13 18:13:50 +08:00
base . ParseLine ( beatmap , section , line ) ;
2017-12-02 00:43:33 +08:00
}
2018-04-13 17:19:50 +08:00
2017-12-02 00:43:33 +08:00
private void handleGeneral ( string line )
{
2018-03-14 17:41:48 +08:00
var pair = SplitKeyVal ( line ) ;
2018-04-13 17:19:50 +08:00
2017-12-02 05:05:01 +08:00
var metadata = beatmap . BeatmapInfo . Metadata ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
switch ( pair . Key )
{
case @"AudioFilename" :
2019-12-11 16:06:56 +08:00
metadata . AudioFile = pair . Value . ToStandardisedPath ( ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"AudioLeadIn" :
2019-03-13 12:56:31 +08:00
beatmap . BeatmapInfo . AudioLeadIn = Parsing . ParseInt ( pair . Value ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"PreviewTime" :
2019-03-13 12:56:31 +08:00
metadata . PreviewTime = getOffsetTime ( Parsing . ParseInt ( pair . Value ) ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"SampleSet" :
2022-12-27 03:36:39 +08:00
defaultSampleBank = Enum . Parse < LegacySampleBank > ( pair . Value ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"SampleVolume" :
2019-03-13 12:56:31 +08:00
defaultSampleVolume = Parsing . ParseInt ( pair . Value ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"StackLeniency" :
2019-03-13 12:56:31 +08:00
beatmap . BeatmapInfo . StackLeniency = Parsing . ParseFloat ( pair . Value ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"Mode" :
2022-01-27 14:19:48 +08:00
int rulesetID = Parsing . ParseInt ( pair . Value ) ;
2018-04-13 17:19:50 +08:00
2023-08-16 18:34:37 +08:00
beatmap . BeatmapInfo . Ruleset = RulesetStore ? . GetRuleset ( rulesetID ) ? ? throw new ArgumentException ( "Ruleset is not available locally." ) ;
2022-01-27 14:19:48 +08:00
switch ( rulesetID )
2017-12-02 00:43:33 +08:00
{
case 0 :
2018-08-15 09:24:56 +08:00
parser = new Rulesets . Objects . Legacy . Osu . ConvertHitObjectParser ( getOffsetTime ( ) , FormatVersion ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case 1 :
2018-08-15 09:24:56 +08:00
parser = new Rulesets . Objects . Legacy . Taiko . ConvertHitObjectParser ( getOffsetTime ( ) , FormatVersion ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case 2 :
2018-08-15 09:24:56 +08:00
parser = new Rulesets . Objects . Legacy . Catch . ConvertHitObjectParser ( getOffsetTime ( ) , FormatVersion ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case 3 :
2018-08-15 09:24:56 +08:00
parser = new Rulesets . Objects . Legacy . Mania . ConvertHitObjectParser ( getOffsetTime ( ) , FormatVersion ) ;
2017-12-02 00:43:33 +08:00
break ;
}
2018-06-08 14:26:27 +08:00
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"LetterboxInBreaks" :
2019-03-13 12:56:31 +08:00
beatmap . BeatmapInfo . LetterboxInBreaks = Parsing . ParseInt ( pair . Value ) = = 1 ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"SpecialStyle" :
2019-03-13 12:56:31 +08:00
beatmap . BeatmapInfo . SpecialStyle = Parsing . ParseInt ( pair . Value ) = = 1 ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"WidescreenStoryboard" :
2019-03-13 12:56:31 +08:00
beatmap . BeatmapInfo . WidescreenStoryboard = Parsing . ParseInt ( pair . Value ) = = 1 ;
2017-12-02 00:43:33 +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 ;
2021-08-25 02:53:27 +08:00
2021-09-12 22:45:27 +08:00
case @"SamplesMatchPlaybackRate" :
beatmap . BeatmapInfo . SamplesMatchPlaybackRate = Parsing . ParseInt ( pair . Value ) = = 1 ;
break ;
2021-08-25 02:53:27 +08:00
case @"Countdown" :
2022-12-27 03:38:35 +08:00
beatmap . BeatmapInfo . Countdown = Enum . Parse < CountdownType > ( pair . Value ) ;
2021-08-25 02:53:27 +08:00
break ;
case @"CountdownOffset" :
beatmap . BeatmapInfo . CountdownOffset = Parsing . ParseInt ( pair . Value ) ;
break ;
2017-12-02 00:43:33 +08:00
}
}
2018-04-13 17:19:50 +08:00
2017-12-02 00:43:33 +08:00
private void handleEditor ( string line )
{
2018-03-14 17:41:48 +08:00
var pair = SplitKeyVal ( line ) ;
2018-04-13 17:19:50 +08:00
2017-12-02 00:43:33 +08:00
switch ( pair . Key )
{
case @"Bookmarks" :
2021-11-23 00:02:22 +08:00
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 ( ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"DistanceSpacing" :
2019-03-13 12:56:31 +08:00
beatmap . BeatmapInfo . DistanceSpacing = Math . Max ( 0 , Parsing . ParseDouble ( pair . Value ) ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"BeatDivisor" :
2019-03-13 12:56:31 +08:00
beatmap . BeatmapInfo . BeatDivisor = Parsing . ParseInt ( pair . Value ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"GridSize" :
2019-03-13 12:56:31 +08:00
beatmap . BeatmapInfo . GridSize = Parsing . ParseInt ( pair . Value ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"TimelineZoom" :
2019-03-13 12:56:31 +08:00
beatmap . BeatmapInfo . TimelineZoom = Math . Max ( 0 , Parsing . ParseDouble ( pair . Value ) ) ;
2017-12-02 00:43:33 +08:00
break ;
}
}
2018-04-13 17:19:50 +08:00
2017-12-02 00:43:33 +08:00
private void handleMetadata ( string line )
{
2018-03-14 17:41:48 +08:00
var pair = SplitKeyVal ( line ) ;
2018-04-13 17:19:50 +08:00
2017-12-02 05:05:01 +08:00
var metadata = beatmap . BeatmapInfo . Metadata ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
switch ( pair . Key )
{
case @"Title" :
metadata . Title = pair . Value ;
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"TitleUnicode" :
metadata . TitleUnicode = pair . Value ;
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"Artist" :
metadata . Artist = pair . Value ;
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"ArtistUnicode" :
metadata . ArtistUnicode = pair . Value ;
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"Creator" :
2022-01-18 22:30:40 +08:00
metadata . Author . Username = pair . Value ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"Version" :
2021-11-11 16:19:53 +08:00
beatmap . BeatmapInfo . DifficultyName = pair . Value ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"Source" :
2020-01-23 23:23:53 +08:00
metadata . Source = pair . Value ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"Tags" :
2020-01-23 23:23:53 +08:00
metadata . Tags = pair . Value ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"BeatmapID" :
2021-11-12 16:45:05 +08:00
beatmap . BeatmapInfo . OnlineID = Parsing . ParseInt ( pair . Value ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"BeatmapSetID" :
2021-11-12 16:50:31 +08:00
beatmap . BeatmapInfo . BeatmapSet = new BeatmapSetInfo { OnlineID = Parsing . ParseInt ( pair . Value ) } ;
2017-12-02 00:43:33 +08:00
break ;
}
}
2018-04-13 17:19:50 +08:00
2017-12-02 00:43:33 +08:00
private void handleDifficulty ( string line )
{
2018-03-14 17:41:48 +08:00
var pair = SplitKeyVal ( line ) ;
2018-04-13 17:19:50 +08:00
2021-10-02 11:34:29 +08:00
var difficulty = beatmap . Difficulty ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
switch ( pair . Key )
{
case @"HPDrainRate" :
2019-03-13 12:56:31 +08:00
difficulty . DrainRate = Parsing . ParseFloat ( pair . Value ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"CircleSize" :
2019-03-13 12:56:31 +08:00
difficulty . CircleSize = Parsing . ParseFloat ( pair . Value ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"OverallDifficulty" :
2019-03-13 12:56:31 +08:00
difficulty . OverallDifficulty = Parsing . ParseFloat ( pair . Value ) ;
2022-01-28 17:53:28 +08:00
if ( ! hasApproachRate )
difficulty . ApproachRate = difficulty . OverallDifficulty ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"ApproachRate" :
2019-03-13 12:56:31 +08:00
difficulty . ApproachRate = Parsing . ParseFloat ( pair . Value ) ;
2022-01-28 17:53:28 +08:00
hasApproachRate = true ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"SliderMultiplier" :
2023-05-08 13:05:59 +08:00
difficulty . SliderMultiplier = Math . Clamp ( Parsing . ParseDouble ( pair . Value ) , 0.4 , 3.6 ) ;
2017-12-02 00:43:33 +08:00
break ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
case @"SliderTickRate" :
2023-05-08 13:05:59 +08:00
difficulty . SliderTickRate = Math . Clamp ( Parsing . ParseDouble ( pair . Value ) , 0.5 , 8 ) ;
2017-12-02 00:43:33 +08:00
break ;
}
}
2018-04-13 17:19:50 +08:00
2018-04-02 19:07:18 +08:00
private void handleEvent ( string line )
2017-12-02 00:43:33 +08:00
{
string [ ] split = line . Split ( ',' ) ;
2018-04-13 17:19:50 +08:00
2019-12-10 19:23:15 +08:00
if ( ! Enum . TryParse ( split [ 0 ] , out LegacyEventType type ) )
2019-08-08 13:44:04 +08:00
throw new InvalidDataException ( $@"Unknown event type: {split[0]}" ) ;
2018-04-13 17:19:50 +08:00
2017-12-02 00:43:33 +08:00
switch ( type )
{
2022-10-19 15:01:07 +08:00
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 ] ) ;
break ;
2023-03-13 17:10:16 +08:00
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.
2023-04-21 08:35:28 +08:00
if ( ! OsuGameBase . VIDEO_EXTENSIONS . Contains ( Path . GetExtension ( filename ) . ToLowerInvariant ( ) ) )
2023-03-13 17:10:16 +08:00
{
beatmap . BeatmapInfo . Metadata . BackgroundFile = filename ;
}
break ;
2019-12-10 19:23:15 +08:00
case LegacyEventType . Background :
2020-01-25 00:05:27 +08:00
beatmap . BeatmapInfo . Metadata . BackgroundFile = CleanFilename ( split [ 2 ] ) ;
2019-08-31 04:19:34 +08:00
break ;
2019-12-10 19:23:15 +08:00
case LegacyEventType . Break :
2019-03-13 12:56:31 +08:00
double start = getOffsetTime ( Parsing . ParseDouble ( split [ 1 ] ) ) ;
2020-04-06 02:29:03 +08:00
double end = Math . Max ( start , getOffsetTime ( Parsing . ParseDouble ( split [ 2 ] ) ) ) ;
2019-03-13 12:56:31 +08:00
2020-10-09 20:04:56 +08:00
beatmap . Breaks . Add ( new BreakPeriod ( start , end ) ) ;
2017-12-02 00:43:33 +08:00
break ;
}
}
2018-04-13 17:19:50 +08:00
2018-04-02 19:07:18 +08:00
private void handleTimingPoint ( string line )
2017-12-02 00:43:33 +08:00
{
2019-08-08 13:44:04 +08:00
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).
2022-08-23 09:44:25 +08:00
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.
2019-08-08 13:44:04 +08:00
double speedMultiplier = beatLength < 0 ? 100.0 / - beatLength : 1 ;
2022-01-23 00:27:27 +08:00
TimeSignature timeSignature = TimeSignature . SimpleQuadruple ;
2019-08-08 13:44:04 +08:00
if ( split . Length > = 3 )
2022-01-23 00:27:27 +08:00
timeSignature = split [ 2 ] [ 0 ] = = '0' ? TimeSignature . SimpleQuadruple : new TimeSignature ( Parsing . ParseInt ( split [ 2 ] ) ) ;
2019-08-08 13:44:04 +08:00
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-02 19:08:40 +08:00
{
2019-12-10 19:19:31 +08:00
LegacyEffectFlags effectFlags = ( LegacyEffectFlags ) Parsing . ParseInt ( split [ 7 ] ) ;
2021-02-25 14:38:56 +08:00
kiaiMode = effectFlags . HasFlagFast ( LegacyEffectFlags . Kiai ) ;
omitFirstBarSignature = effectFlags . HasFlagFast ( LegacyEffectFlags . OmitFirstBarLine ) ;
2018-04-02 19:08:40 +08:00
}
2019-08-08 13:44:04 +08:00
string stringSampleSet = sampleSet . ToString ( ) . ToLowerInvariant ( ) ;
if ( stringSampleSet = = @"none" )
2022-10-19 22:54:12 +08:00
stringSampleSet = HitSampleInfo . BANK_NORMAL ;
2019-08-08 13:44:04 +08:00
if ( timingChange )
2019-03-13 10:30:33 +08:00
{
2022-08-23 09:44:25 +08:00
if ( double . IsNaN ( beatLength ) )
throw new InvalidDataException ( "Beat length cannot be NaN in a timing control point" ) ;
2019-08-08 13:44:04 +08:00
var controlPoint = CreateTimingControlPoint ( ) ;
2019-10-25 18:58:42 +08:00
2019-08-08 13:44:04 +08:00
controlPoint . BeatLength = beatLength ;
controlPoint . TimeSignature = timeSignature ;
2023-02-28 18:29:31 +08:00
controlPoint . OmitFirstBarLine = omitFirstBarSignature ;
2019-08-08 13:44:04 +08:00
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
2022-10-13 14:05:15 +08:00
int onlineRulesetID = beatmap . BeatmapInfo . Ruleset . OnlineID ;
addControlPoint ( time , new LegacyDifficultyControlPoint ( onlineRulesetID , beatLength )
2021-09-18 21:32:08 +08:00
{
SliderVelocity = speedMultiplier ,
} , timingChange ) ;
2019-08-08 13:44:04 +08:00
2021-09-18 20:24:50 +08:00
var effectPoint = new EffectControlPoint
2019-08-08 13:44:04 +08:00
{
KiaiMode = kiaiMode ,
2021-09-18 20:24:50 +08:00
} ;
2022-05-08 20:49:42 +08:00
// osu!taiko and osu!mania use effect points rather than difficulty points for scroll speed adjustments.
if ( onlineRulesetID = = 1 | | onlineRulesetID = = 3 )
2021-09-18 20:24:50 +08:00
effectPoint . ScrollSpeed = speedMultiplier ;
addControlPoint ( time , effectPoint , timingChange ) ;
2019-08-08 13:44:04 +08:00
2019-10-25 18:58:42 +08:00
addControlPoint ( time , new LegacySampleControlPoint
2019-08-08 13:44:04 +08:00
{
SampleBank = stringSampleSet ,
SampleVolume = sampleVolume ,
CustomSampleBank = customSampleBank ,
2019-10-25 18:58:42 +08:00
} , timingChange ) ;
}
private readonly List < ControlPoint > pendingControlPoints = new List < ControlPoint > ( ) ;
2020-04-21 13:19:05 +08:00
private readonly HashSet < Type > pendingControlPointTypes = new HashSet < Type > ( ) ;
2019-10-25 18:58:42 +08:00
private double pendingControlPointsTime ;
2022-01-28 17:53:28 +08:00
private bool hasApproachRate ;
2019-10-25 18:58:42 +08:00
private void addControlPoint ( double time , ControlPoint point , bool timingChange )
{
2019-10-30 17:02:18 +08:00
if ( time ! = pendingControlPointsTime )
flushPendingPoints ( ) ;
2019-10-25 18:58:42 +08:00
if ( timingChange )
2020-04-21 13:19:05 +08:00
pendingControlPoints . Insert ( 0 , point ) ;
else
pendingControlPoints . Add ( point ) ;
2019-10-25 18:58:42 +08:00
pendingControlPointsTime = time ;
}
private void flushPendingPoints ( )
{
2020-04-21 13:19:05 +08:00
// 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 ( ) ;
if ( pendingControlPointTypes . Contains ( type ) )
continue ;
pendingControlPointTypes . Add ( type ) ;
beatmap . ControlPointInfo . Add ( pendingControlPointsTime , pendingControlPoints [ i ] ) ;
}
2019-10-25 18:58:42 +08:00
pendingControlPoints . Clear ( ) ;
2020-04-21 13:19:05 +08:00
pendingControlPointTypes . Clear ( ) ;
2017-12-02 00:43:33 +08:00
}
2018-04-13 17:19:50 +08:00
2018-04-02 19:07:18 +08:00
private void handleHitObject ( string line )
2017-12-02 00:43:33 +08:00
{
// If the ruleset wasn't specified, assume the osu!standard ruleset.
2020-06-03 15:48:44 +08:00
parser ? ? = new Rulesets . Objects . Legacy . Osu . ConvertHitObjectParser ( getOffsetTime ( ) , FormatVersion ) ;
2018-04-13 17:19:50 +08:00
2018-08-15 09:24:56 +08:00
var obj = parser . Parse ( line ) ;
2021-08-25 17:00:57 +08:00
2017-12-02 00:43:33 +08:00
if ( obj ! = null )
2021-08-25 17:00:57 +08:00
{
2021-10-02 11:34:29 +08:00
obj . ApplyDefaults ( beatmap . ControlPointInfo , beatmap . Difficulty ) ;
2021-08-25 17:00:57 +08:00
2017-12-02 05:05:01 +08:00
beatmap . HitObjects . Add ( obj ) ;
2021-08-25 17:00:57 +08:00
}
2017-12-02 00:43:33 +08:00
}
2018-04-13 17:19:50 +08:00
2018-03-04 21:13:43 +08:00
private int getOffsetTime ( int time ) = > time + ( ApplyOffsets ? offset : 0 ) ;
2018-04-13 17:19:50 +08:00
2018-04-30 15:43:32 +08:00
private double getOffsetTime ( ) = > ApplyOffsets ? offset : 0 ;
2018-03-04 21:13:43 +08:00
private double getOffsetTime ( double time ) = > time + ( ApplyOffsets ? offset : 0 ) ;
2018-07-16 15:26:37 +08:00
2018-10-09 10:34:38 +08:00
protected virtual TimingControlPoint CreateTimingControlPoint ( ) = > new TimingControlPoint ( ) ;
2017-12-02 00:43:33 +08:00
}
}