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
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 ;
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 ;
2020-03-08 06:08:13 +08:00
using osu.Game.Beatmaps.Timing ;
using osu.Game.IO ;
using osu.Game.Rulesets.Objects.Legacy ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Beatmaps.Formats
{
public class LegacyBeatmapDecoder : LegacyDecoder < Beatmap >
{
public const int LATEST_VERSION = 14 ;
private Beatmap beatmap ;
private ConvertHitObjectParser parser ;
private LegacySampleBank defaultSampleBank ;
private int defaultSampleVolume = 100 ;
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-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 ;
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 )
2018-04-13 17:19:50 +08:00
{
// BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off)
2018-05-28 02:00:21 +08:00
offset = FormatVersion < 5 ? 24 : 0 ;
2018-04-13 17:19:50 +08:00
}
2019-09-10 06:43:30 +08:00
protected override void ParseStreamInto ( LineBufferedReader stream , Beatmap beatmap )
2018-04-13 17:19:50 +08:00
{
this . beatmap = beatmap ;
this . beatmap . BeatmapInfo . BeatmapVersion = FormatVersion ;
base . ParseStreamInto ( stream , 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
foreach ( var hitObject in this . beatmap . HitObjects )
hitObject . ApplyDefaults ( this . beatmap . ControlPointInfo , this . beatmap . BeatmapInfo . BaseDifficulty ) ;
}
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
protected override void ParseLine ( Beatmap beatmap , Section section , string line )
{
2018-07-16 07:54:20 +08:00
var strippedLine = StripComments ( line ) ;
2018-07-16 07:04:41 +08:00
2018-04-13 17:19:50 +08:00
switch ( section )
{
case Section . General :
2018-07-16 07:54:20 +08:00
handleGeneral ( strippedLine ) ;
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 :
2018-07-16 07:54:20 +08:00
handleEditor ( strippedLine ) ;
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 :
2018-07-16 07:54:20 +08:00
handleDifficulty ( strippedLine ) ;
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 :
2018-07-16 07:54:20 +08:00
handleEvent ( strippedLine ) ;
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 :
2018-07-16 07:54:20 +08:00
handleTimingPoint ( strippedLine ) ;
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 :
2018-07-16 07:54:20 +08:00
handleHitObject ( strippedLine ) ;
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" :
2019-03-13 12:56:31 +08:00
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" :
2019-03-13 12:56:31 +08:00
metadata . PreviewTime = getOffsetTime ( 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 @"Countdown" :
2019-03-13 12:56:31 +08:00
beatmap . BeatmapInfo . Countdown = 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 @"SampleSet" :
defaultSampleBank = ( LegacySampleBank ) Enum . Parse ( typeof ( LegacySampleBank ) , pair . Value ) ;
break ;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"SampleVolume" :
2019-03-13 12:56:31 +08:00
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" :
2019-03-13 12:56:31 +08:00
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" :
2019-03-13 12:56:31 +08:00
beatmap . BeatmapInfo . RulesetID = Parsing . ParseInt ( pair . Value ) ;
2018-04-13 17:19:50 +08:00
switch ( beatmap . BeatmapInfo . RulesetID )
{
case 0 :
2018-08-15 09:24:56 +08:00
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 :
2018-08-15 09:24:56 +08:00
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 :
2018-08-15 09:24:56 +08:00
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 :
2018-08-15 09:24:56 +08:00
parser = new Rulesets . Objects . Legacy . Mania . ConvertHitObjectParser ( getOffsetTime ( ) , FormatVersion ) ;
2018-04-13 17:19:50 +08:00
break ;
}
2018-06-08 14:26:27 +08:00
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" :
2019-03-13 12:56:31 +08:00
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" :
2019-03-13 12:56:31 +08:00
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" :
2019-03-13 12:56:31 +08:00
beatmap . BeatmapInfo . WidescreenStoryboard = Parsing . ParseInt ( pair . Value ) = = 1 ;
2018-04-13 17:19:50 +08:00
break ;
}
}
private void handleEditor ( string line )
{
var pair = SplitKeyVal ( line ) ;
switch ( pair . Key )
{
case @"Bookmarks" :
beatmap . BeatmapInfo . StoredBookmarks = pair . Value ;
break ;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"DistanceSpacing" :
2019-03-13 12:56:31 +08:00
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" :
2019-03-13 12:56:31 +08:00
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" :
2019-03-13 12:56:31 +08:00
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" :
2019-03-13 12:56:31 +08:00
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" :
metadata . AuthorString = pair . Value ;
break ;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"Version" :
beatmap . BeatmapInfo . Version = pair . Value ;
break ;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case @"Source" :
2020-01-23 23:23:53 +08:00
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" :
2020-01-23 23:23:53 +08:00
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" :
2019-03-13 12:56:31 +08:00
beatmap . BeatmapInfo . OnlineBeatmapID = 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" :
2019-03-13 12:56:31 +08:00
beatmap . BeatmapInfo . BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = 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 . BeatmapInfo . BaseDifficulty ;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
switch ( pair . Key )
{
case @"HPDrainRate" :
2019-03-13 12:56:31 +08:00
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" :
2019-03-13 12:56:31 +08:00
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" :
2019-03-13 12:56:31 +08:00
difficulty . OverallDifficulty = 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 @"ApproachRate" :
2019-03-13 12:56:31 +08:00
difficulty . ApproachRate = 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 @"SliderMultiplier" :
2019-03-13 12:56:31 +08:00
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" :
2019-03-13 12:56:31 +08:00
difficulty . SliderTickRate = Parsing . ParseDouble ( pair . Value ) ;
2018-04-13 17:19:50 +08:00
break ;
}
}
private void handleEvent ( string line )
{
string [ ] split = line . Split ( ',' ) ;
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
switch ( type )
{
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-04-06 02:29:03 +08:00
var breakEvent = new BreakPeriod ( start , end ) ;
2018-04-13 17:19:50 +08:00
if ( ! breakEvent . HasEffect )
return ;
beatmap . Breaks . Add ( breakEvent ) ;
break ;
}
}
private void handleTimingPoint ( string line )
{
2019-08-08 13:44:04 +08:00
string [ ] split = line . Split ( ',' ) ;
double time = getOffsetTime ( Parsing . ParseDouble ( split [ 0 ] . Trim ( ) ) ) ;
double beatLength = Parsing . ParseDouble ( split [ 1 ] . Trim ( ) ) ;
double speedMultiplier = beatLength < 0 ? 100.0 / - beatLength : 1 ;
TimeSignatures timeSignature = TimeSignatures . SimpleQuadruple ;
if ( split . Length > = 3 )
timeSignature = split [ 2 ] [ 0 ] = = '0' ? TimeSignatures . SimpleQuadruple : ( TimeSignatures ) 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 ] ) ;
kiaiMode = effectFlags . HasFlag ( LegacyEffectFlags . Kiai ) ;
omitFirstBarSignature = effectFlags . HasFlag ( LegacyEffectFlags . OmitFirstBarLine ) ;
2018-04-13 17:19:50 +08:00
}
2019-08-08 13:44:04 +08:00
string stringSampleSet = sampleSet . ToString ( ) . ToLowerInvariant ( ) ;
if ( stringSampleSet = = @"none" )
stringSampleSet = @"normal" ;
if ( timingChange )
2019-03-13 10:30:33 +08:00
{
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 ;
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
2020-07-13 16:06:00 +08:00
#pragma warning disable 618
addControlPoint ( time , new LegacyDifficultyControlPoint ( beatLength )
#pragma warning restore 618
2018-04-13 17:19:50 +08:00
{
2019-10-25 18:58:42 +08:00
SpeedMultiplier = speedMultiplier ,
} , timingChange ) ;
2019-08-08 13:44:04 +08:00
2019-10-25 18:58:42 +08:00
addControlPoint ( time , new EffectControlPoint
2019-08-08 13:44:04 +08:00
{
KiaiMode = kiaiMode ,
OmitFirstBarLine = omitFirstBarSignature ,
2019-10-25 18:58:42 +08:00
} , 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 ;
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 ( ) ;
2018-04-13 17:19:50 +08:00
}
private void handleHitObject ( string line )
{
// 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 ) ;
2018-04-13 17:19:50 +08:00
if ( obj ! = null )
beatmap . HitObjects . Add ( obj ) ;
}
private int getOffsetTime ( int time ) = > time + ( ApplyOffsets ? offset : 0 ) ;
2018-04-30 15:43:32 +08:00
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
2018-10-09 10:34:38 +08:00
protected virtual TimingControlPoint CreateTimingControlPoint ( ) = > new TimingControlPoint ( ) ;
2018-04-13 17:19:50 +08:00
}
}