2019-12-10 11:44:45 +00: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.
using System ;
using System.Collections.Generic ;
2020-04-21 05:55:17 +00:00
using System.Globalization ;
2019-12-10 11:44:45 +00:00
using System.IO ;
using System.Linq ;
using System.Text ;
2020-08-12 04:37:25 +00:00
using JetBrains.Annotations ;
2019-12-10 11:44:45 +00:00
using osu.Game.Audio ;
using osu.Game.Beatmaps.ControlPoints ;
using osu.Game.Beatmaps.Legacy ;
using osu.Game.Rulesets.Objects ;
2020-04-21 05:55:17 +00:00
using osu.Game.Rulesets.Objects.Legacy ;
2019-12-10 11:44:45 +00:00
using osu.Game.Rulesets.Objects.Types ;
2020-08-10 03:21:10 +00:00
using osu.Game.Skinning ;
2020-04-21 07:04:58 +00:00
using osuTK ;
2020-08-23 13:08:02 +00:00
using osuTK.Graphics ;
2019-12-10 11:44:45 +00:00
namespace osu.Game.Beatmaps.Formats
{
public class LegacyBeatmapEncoder
{
2019-12-13 10:11:45 +00:00
public const int LATEST_VERSION = 128 ;
2019-12-10 11:44:45 +00:00
private readonly IBeatmap beatmap ;
2020-08-31 15:24:03 +00:00
[CanBeNull]
2020-09-01 15:58:06 +00:00
private readonly ISkin skin ;
2020-08-23 13:08:02 +00:00
/// <summary>
/// Creates a new <see cref="LegacyBeatmapEncoder"/>.
/// </summary>
/// <param name="beatmap">The beatmap to encode.</param>
2020-08-30 14:07:58 +00:00
/// <param name="skin">The beatmap's skin, used for encoding combo colours.</param>
2020-09-01 15:58:06 +00:00
public LegacyBeatmapEncoder ( IBeatmap beatmap , [ CanBeNull ] ISkin skin )
2019-12-10 11:44:45 +00:00
{
this . beatmap = beatmap ;
2020-08-15 20:03:24 +00:00
this . skin = skin ;
2019-12-10 11:44:45 +00:00
if ( beatmap . BeatmapInfo . RulesetID < 0 | | beatmap . BeatmapInfo . RulesetID > 3 )
throw new ArgumentException ( "Only beatmaps in the osu, taiko, catch, or mania rulesets can be encoded to the legacy beatmap format." , nameof ( beatmap ) ) ;
}
public void Encode ( TextWriter writer )
{
writer . WriteLine ( $"osu file format v{LATEST_VERSION}" ) ;
writer . WriteLine ( ) ;
handleGeneral ( writer ) ;
writer . WriteLine ( ) ;
handleEditor ( writer ) ;
writer . WriteLine ( ) ;
handleMetadata ( writer ) ;
writer . WriteLine ( ) ;
handleDifficulty ( writer ) ;
writer . WriteLine ( ) ;
handleEvents ( writer ) ;
writer . WriteLine ( ) ;
2020-04-21 06:05:24 +00:00
handleControlPoints ( writer ) ;
2019-12-10 11:44:45 +00:00
2020-08-10 03:21:10 +00:00
writer . WriteLine ( ) ;
2020-08-31 15:24:03 +00:00
handleColours ( writer ) ;
2020-08-10 03:21:10 +00:00
2019-12-10 11:44:45 +00:00
writer . WriteLine ( ) ;
handleHitObjects ( writer ) ;
}
private void handleGeneral ( TextWriter writer )
{
writer . WriteLine ( "[General]" ) ;
2020-04-21 06:04:04 +00:00
if ( beatmap . Metadata . AudioFile ! = null ) writer . WriteLine ( FormattableString . Invariant ( $"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}" ) ) ;
2019-12-10 11:44:45 +00:00
writer . WriteLine ( FormattableString . Invariant ( $"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"PreviewTime: {beatmap.Metadata.PreviewTime}" ) ) ;
// Todo: Not all countdown types are supported by lazer yet
2019-12-16 08:06:52 +00:00
writer . WriteLine ( FormattableString . Invariant ( $"Countdown: {(beatmap.BeatmapInfo.Countdown ? '1' : '0')}" ) ) ;
2020-01-02 04:20:38 +00:00
writer . WriteLine ( FormattableString . Invariant ( $"SampleSet: {toLegacySampleBank(beatmap.ControlPointInfo.SamplePointAt(double.MinValue).SampleBank)}" ) ) ;
2019-12-10 11:44:45 +00:00
writer . WriteLine ( FormattableString . Invariant ( $"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"Mode: {beatmap.BeatmapInfo.RulesetID}" ) ) ;
2019-12-16 08:06:52 +00:00
writer . WriteLine ( FormattableString . Invariant ( $"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}" ) ) ;
2019-12-10 11:44:45 +00:00
// if (beatmap.BeatmapInfo.UseSkinSprites)
// writer.WriteLine(@"UseSkinSprites: 1");
// if (b.AlwaysShowPlayfield)
// writer.WriteLine(@"AlwaysShowPlayfield: 1");
// if (b.OverlayPosition != OverlayPosition.NoChange)
// writer.WriteLine(@"OverlayPosition: " + b.OverlayPosition);
// if (!string.IsNullOrEmpty(b.SkinPreference))
// writer.WriteLine(@"SkinPreference:" + b.SkinPreference);
// if (b.EpilepsyWarning)
// writer.WriteLine(@"EpilepsyWarning: 1");
// if (b.CountdownOffset > 0)
// writer.WriteLine(@"CountdownOffset: " + b.CountdownOffset.ToString());
if ( beatmap . BeatmapInfo . RulesetID = = 3 )
2019-12-16 08:06:52 +00:00
writer . WriteLine ( FormattableString . Invariant ( $"SpecialStyle: {(beatmap.BeatmapInfo.SpecialStyle ? '1' : '0')}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"WidescreenStoryboard: {(beatmap.BeatmapInfo.WidescreenStoryboard ? '1' : '0')}" ) ) ;
2019-12-10 11:44:45 +00:00
// if (b.SamplesMatchPlaybackRate)
// writer.WriteLine(@"SamplesMatchPlaybackRate: 1");
}
private void handleEditor ( TextWriter writer )
{
writer . WriteLine ( "[Editor]" ) ;
if ( beatmap . BeatmapInfo . Bookmarks . Length > 0 )
writer . WriteLine ( FormattableString . Invariant ( $"Bookmarks: {string.Join(',', beatmap.BeatmapInfo.Bookmarks)}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"DistanceSpacing: {beatmap.BeatmapInfo.DistanceSpacing}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"BeatDivisor: {beatmap.BeatmapInfo.BeatDivisor}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"GridSize: {beatmap.BeatmapInfo.GridSize}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"TimelineZoom: {beatmap.BeatmapInfo.TimelineZoom}" ) ) ;
}
private void handleMetadata ( TextWriter writer )
{
writer . WriteLine ( "[Metadata]" ) ;
writer . WriteLine ( FormattableString . Invariant ( $"Title: {beatmap.Metadata.Title}" ) ) ;
2020-04-21 06:04:04 +00:00
if ( beatmap . Metadata . TitleUnicode ! = null ) writer . WriteLine ( FormattableString . Invariant ( $"TitleUnicode: {beatmap.Metadata.TitleUnicode}" ) ) ;
2019-12-10 11:44:45 +00:00
writer . WriteLine ( FormattableString . Invariant ( $"Artist: {beatmap.Metadata.Artist}" ) ) ;
2020-04-21 06:04:04 +00:00
if ( beatmap . Metadata . ArtistUnicode ! = null ) writer . WriteLine ( FormattableString . Invariant ( $"ArtistUnicode: {beatmap.Metadata.ArtistUnicode}" ) ) ;
2019-12-10 11:44:45 +00:00
writer . WriteLine ( FormattableString . Invariant ( $"Creator: {beatmap.Metadata.AuthorString}" ) ) ;
2019-12-13 08:01:59 +00:00
writer . WriteLine ( FormattableString . Invariant ( $"Version: {beatmap.BeatmapInfo.Version}" ) ) ;
2020-04-21 06:04:04 +00:00
if ( beatmap . Metadata . Source ! = null ) writer . WriteLine ( FormattableString . Invariant ( $"Source: {beatmap.Metadata.Source}" ) ) ;
if ( beatmap . Metadata . Tags ! = null ) writer . WriteLine ( FormattableString . Invariant ( $"Tags: {beatmap.Metadata.Tags}" ) ) ;
if ( beatmap . BeatmapInfo . OnlineBeatmapID ! = null ) writer . WriteLine ( FormattableString . Invariant ( $"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID}" ) ) ;
if ( beatmap . BeatmapInfo . BeatmapSet ? . OnlineBeatmapSetID ! = null ) writer . WriteLine ( FormattableString . Invariant ( $"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID}" ) ) ;
2019-12-10 11:44:45 +00:00
}
private void handleDifficulty ( TextWriter writer )
{
writer . WriteLine ( "[Difficulty]" ) ;
writer . WriteLine ( FormattableString . Invariant ( $"HPDrainRate: {beatmap.BeatmapInfo.BaseDifficulty.DrainRate}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"CircleSize: {beatmap.BeatmapInfo.BaseDifficulty.CircleSize}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"OverallDifficulty: {beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"ApproachRate: {beatmap.BeatmapInfo.BaseDifficulty.ApproachRate}" ) ) ;
2020-04-21 07:45:01 +00:00
// Taiko adjusts the slider multiplier (see: TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER)
writer . WriteLine ( beatmap . BeatmapInfo . RulesetID = = 1
? FormattableString . Invariant ( $"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / 1.4f}" )
: FormattableString . Invariant ( $"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}" ) ) ;
2019-12-10 11:44:45 +00:00
writer . WriteLine ( FormattableString . Invariant ( $"SliderTickRate: {beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate}" ) ) ;
}
private void handleEvents ( TextWriter writer )
{
2019-12-12 09:48:22 +00:00
writer . WriteLine ( "[Events]" ) ;
2019-12-12 09:49:47 +00:00
if ( ! string . IsNullOrEmpty ( beatmap . BeatmapInfo . Metadata . BackgroundFile ) )
2019-12-12 09:51:05 +00:00
writer . WriteLine ( FormattableString . Invariant ( $"{(int)LegacyEventType.Background},0,\" { beatmap . BeatmapInfo . Metadata . BackgroundFile } \ ",0,0" ) ) ;
2019-12-12 09:49:47 +00:00
2019-12-12 09:47:28 +00:00
foreach ( var b in beatmap . Breaks )
writer . WriteLine ( FormattableString . Invariant ( $"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}" ) ) ;
2019-12-10 11:44:45 +00:00
}
2020-04-21 06:05:24 +00:00
private void handleControlPoints ( TextWriter writer )
2019-12-10 11:44:45 +00:00
{
if ( beatmap . ControlPointInfo . Groups . Count = = 0 )
return ;
writer . WriteLine ( "[TimingPoints]" ) ;
foreach ( var group in beatmap . ControlPointInfo . Groups )
{
2020-04-21 06:05:24 +00:00
var groupTimingPoint = group . ControlPoints . OfType < TimingControlPoint > ( ) . FirstOrDefault ( ) ;
// If the group contains a timing control point, it needs to be output separately.
if ( groupTimingPoint ! = null )
{
writer . Write ( FormattableString . Invariant ( $"{groupTimingPoint.Time}," ) ) ;
writer . Write ( FormattableString . Invariant ( $"{groupTimingPoint.BeatLength}," ) ) ;
outputControlPointEffectsAt ( groupTimingPoint . Time , true ) ;
}
// Output any remaining effects as secondary non-timing control point.
2019-12-10 11:44:45 +00:00
var difficultyPoint = beatmap . ControlPointInfo . DifficultyPointAt ( group . Time ) ;
2020-04-21 06:05:24 +00:00
writer . Write ( FormattableString . Invariant ( $"{group.Time}," ) ) ;
writer . Write ( FormattableString . Invariant ( $"{-100 / difficultyPoint.SpeedMultiplier}," ) ) ;
outputControlPointEffectsAt ( group . Time , false ) ;
}
2019-12-10 11:44:45 +00:00
2020-04-21 06:05:24 +00:00
void outputControlPointEffectsAt ( double time , bool isTimingPoint )
{
var samplePoint = beatmap . ControlPointInfo . SamplePointAt ( time ) ;
var effectPoint = beatmap . ControlPointInfo . EffectPointAt ( time ) ;
2019-12-10 11:44:45 +00:00
// Apply the control point to a hit sample to uncover legacy properties (e.g. suffix)
2020-12-01 06:37:51 +00:00
HitSampleInfo tempHitSample = samplePoint . ApplyTo ( new ConvertHitObjectParser . LegacyHitSampleInfo ( string . Empty ) ) ;
2019-12-10 11:44:45 +00:00
// Convert effect flags to the legacy format
LegacyEffectFlags effectFlags = LegacyEffectFlags . None ;
if ( effectPoint . KiaiMode )
effectFlags | = LegacyEffectFlags . Kiai ;
if ( effectPoint . OmitFirstBarLine )
effectFlags | = LegacyEffectFlags . OmitFirstBarLine ;
2020-04-21 06:05:24 +00:00
writer . Write ( FormattableString . Invariant ( $"{(int)beatmap.ControlPointInfo.TimingPointAt(time).TimeSignature}," ) ) ;
2019-12-10 11:44:45 +00:00
writer . Write ( FormattableString . Invariant ( $"{(int)toLegacySampleBank(tempHitSample.Bank)}," ) ) ;
2020-04-21 05:55:17 +00:00
writer . Write ( FormattableString . Invariant ( $"{toLegacyCustomSampleBank(tempHitSample)}," ) ) ;
2019-12-10 11:44:45 +00:00
writer . Write ( FormattableString . Invariant ( $"{tempHitSample.Volume}," ) ) ;
2020-04-21 06:05:24 +00:00
writer . Write ( FormattableString . Invariant ( $"{(isTimingPoint ? '1' : '0')}," ) ) ;
2019-12-10 11:44:45 +00:00
writer . Write ( FormattableString . Invariant ( $"{(int)effectFlags}" ) ) ;
2019-12-16 08:08:46 +00:00
writer . WriteLine ( ) ;
2019-12-10 11:44:45 +00:00
}
}
2020-08-31 15:24:03 +00:00
private void handleColours ( TextWriter writer )
2020-08-10 03:21:10 +00:00
{
2020-08-31 15:24:03 +00:00
var colours = skin ? . GetConfig < GlobalSkinColours , IReadOnlyList < Color4 > > ( GlobalSkinColours . ComboColours ) ? . Value ;
2020-08-10 03:21:10 +00:00
2020-08-12 04:37:33 +00:00
if ( colours = = null | | colours . Count = = 0 )
2020-08-10 03:21:10 +00:00
return ;
writer . WriteLine ( "[Colours]" ) ;
for ( var i = 0 ; i < colours . Count ; i + + )
{
var comboColour = colours [ i ] ;
2020-08-23 13:08:02 +00:00
writer . Write ( FormattableString . Invariant ( $"Combo{i}: " ) ) ;
writer . Write ( FormattableString . Invariant ( $"{(byte)(comboColour.R * byte.MaxValue)}," ) ) ;
writer . Write ( FormattableString . Invariant ( $"{(byte)(comboColour.G * byte.MaxValue)}," ) ) ;
writer . Write ( FormattableString . Invariant ( $"{(byte)(comboColour.B * byte.MaxValue)}," ) ) ;
2020-08-30 14:11:49 +00:00
writer . Write ( FormattableString . Invariant ( $"{(byte)(comboColour.A * byte.MaxValue)}" ) ) ;
writer . WriteLine ( ) ;
2020-08-10 03:21:10 +00:00
}
}
2019-12-10 11:44:45 +00:00
private void handleHitObjects ( TextWriter writer )
{
2020-11-07 15:17:23 +00:00
writer . WriteLine ( "[HitObjects]" ) ;
2019-12-10 11:44:45 +00:00
if ( beatmap . HitObjects . Count = = 0 )
return ;
2020-04-21 07:04:58 +00:00
foreach ( var h in beatmap . HitObjects )
handleHitObject ( writer , h ) ;
}
private void handleHitObject ( TextWriter writer , HitObject hitObject )
{
Vector2 position = new Vector2 ( 256 , 192 ) ;
2019-12-16 07:57:40 +00:00
switch ( beatmap . BeatmapInfo . RulesetID )
2019-12-10 11:44:45 +00:00
{
2019-12-16 07:57:40 +00:00
case 0 :
2020-04-21 07:04:58 +00:00
position = ( ( IHasPosition ) hitObject ) . Position ;
2019-12-16 07:57:40 +00:00
break ;
2019-12-10 11:44:45 +00:00
2020-04-21 07:04:58 +00:00
case 2 :
2020-07-01 15:21:45 +00:00
position . X = ( ( IHasXPosition ) hitObject ) . X ;
2020-04-21 07:04:58 +00:00
break ;
case 3 :
int totalColumns = ( int ) Math . Max ( 1 , beatmap . BeatmapInfo . BaseDifficulty . CircleSize ) ;
position . X = ( int ) Math . Ceiling ( ( ( IHasXPosition ) hitObject ) . X * ( 512f / totalColumns ) ) ;
break ;
}
2019-12-10 11:44:45 +00:00
2020-04-21 07:04:58 +00:00
writer . Write ( FormattableString . Invariant ( $"{position.X}," ) ) ;
writer . Write ( FormattableString . Invariant ( $"{position.Y}," ) ) ;
2019-12-10 11:44:45 +00:00
writer . Write ( FormattableString . Invariant ( $"{hitObject.StartTime}," ) ) ;
2019-12-18 08:35:51 +00:00
writer . Write ( FormattableString . Invariant ( $"{(int)getObjectType(hitObject)}," ) ) ;
2020-04-21 07:04:58 +00:00
writer . Write ( FormattableString . Invariant ( $"{(int)toLegacyHitSoundType(hitObject.Samples)}," ) ) ;
2019-12-10 11:44:45 +00:00
2020-05-31 13:30:55 +00:00
if ( hitObject is IHasPath path )
2019-12-10 11:44:45 +00:00
{
2020-05-31 13:30:55 +00:00
addPathData ( writer , path , position ) ;
2019-12-18 08:35:51 +00:00
writer . Write ( getSampleBank ( hitObject . Samples , zeroBanks : true ) ) ;
}
else
{
2020-05-27 03:38:39 +00:00
if ( hitObject is IHasDuration )
2020-04-22 07:40:07 +00:00
addEndTimeData ( writer , hitObject ) ;
2019-12-18 08:35:51 +00:00
writer . Write ( getSampleBank ( hitObject . Samples ) ) ;
}
2019-12-12 10:52:15 +00:00
2019-12-18 08:35:51 +00:00
writer . WriteLine ( ) ;
}
2020-04-22 07:27:07 +00:00
private LegacyHitObjectType getObjectType ( HitObject hitObject )
2019-12-18 08:35:51 +00:00
{
2020-04-21 07:04:58 +00:00
LegacyHitObjectType type = 0 ;
2019-12-18 08:35:51 +00:00
2020-04-21 07:04:58 +00:00
if ( hitObject is IHasCombo combo )
{
type = ( LegacyHitObjectType ) ( combo . ComboOffset < < 4 ) ;
2019-12-18 08:35:51 +00:00
2020-04-21 07:04:58 +00:00
if ( combo . NewCombo )
type | = LegacyHitObjectType . NewCombo ;
}
2019-12-10 11:44:45 +00:00
2019-12-18 08:35:51 +00:00
switch ( hitObject )
{
2020-05-26 08:44:47 +00:00
case IHasPath _ :
2019-12-18 08:35:51 +00:00
type | = LegacyHitObjectType . Slider ;
break ;
2020-05-27 03:38:39 +00:00
case IHasDuration _ :
2020-04-22 07:27:07 +00:00
if ( beatmap . BeatmapInfo . RulesetID = = 3 )
type | = LegacyHitObjectType . Hold ;
else
type | = LegacyHitObjectType . Spinner ;
2019-12-18 08:35:51 +00:00
break ;
default :
type | = LegacyHitObjectType . Circle ;
break ;
}
return type ;
}
2020-05-26 08:44:47 +00:00
private void addPathData ( TextWriter writer , IHasPath pathData , Vector2 position )
2019-12-18 08:35:51 +00:00
{
PathType ? lastType = null ;
2020-05-26 08:44:47 +00:00
for ( int i = 0 ; i < pathData . Path . ControlPoints . Count ; i + + )
2019-12-18 08:35:51 +00:00
{
2020-05-26 08:44:47 +00:00
PathControlPoint point = pathData . Path . ControlPoints [ i ] ;
2019-12-18 08:35:51 +00:00
if ( point . Type . Value ! = null )
{
2021-04-06 05:10:59 +00:00
// We've reached a new (explicit) segment!
2021-04-05 10:59:54 +00:00
2021-04-06 05:10:59 +00:00
// Explicit segments have a new format in which the type is injected into the middle of the control point string.
// To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point.
bool needsExplicitSegment = point . Type . Value ! = lastType ;
2021-04-05 10:59:54 +00:00
2021-04-06 05:10:59 +00:00
// One exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable.
// Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder.
if ( i > 1 )
{
// We need to use the absolute control point position to determine equality, otherwise floating point issues may arise.
Vector2 p1 = position + pathData . Path . ControlPoints [ i - 1 ] . Position . Value ;
Vector2 p2 = position + pathData . Path . ControlPoints [ i - 2 ] . Position . Value ;
if ( ( int ) p1 . X = = ( int ) p2 . X & & ( int ) p1 . Y = = ( int ) p2 . Y )
needsExplicitSegment = true ;
}
if ( needsExplicitSegment )
2019-12-10 11:44:45 +00:00
{
2019-12-18 08:35:51 +00:00
switch ( point . Type . Value )
2019-12-12 10:52:15 +00:00
{
2019-12-18 08:35:51 +00:00
case PathType . Bezier :
writer . Write ( "B|" ) ;
break ;
case PathType . Catmull :
writer . Write ( "C|" ) ;
break ;
case PathType . PerfectCurve :
writer . Write ( "P|" ) ;
break ;
case PathType . Linear :
writer . Write ( "L|" ) ;
break ;
2019-12-12 10:52:15 +00:00
}
2019-12-10 11:44:45 +00:00
2019-12-18 08:35:51 +00:00
lastType = point . Type . Value ;
}
else
2019-12-12 10:01:15 +00:00
{
2019-12-18 08:35:51 +00:00
// New segment with the same type - duplicate the control point
2020-04-21 07:04:58 +00:00
writer . Write ( FormattableString . Invariant ( $"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}|" ) ) ;
2019-12-12 10:01:15 +00:00
}
2019-12-10 11:44:45 +00:00
}
2019-12-18 08:35:51 +00:00
if ( i ! = 0 )
2019-12-10 11:44:45 +00:00
{
2020-04-21 07:04:58 +00:00
writer . Write ( FormattableString . Invariant ( $"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}" ) ) ;
2020-05-26 08:44:47 +00:00
writer . Write ( i ! = pathData . Path . ControlPoints . Count - 1 ? "|" : "," ) ;
2019-12-10 11:44:45 +00:00
}
}
2020-05-26 08:44:47 +00:00
var curveData = pathData as IHasPathWithRepeats ;
2019-12-12 11:04:46 +00:00
2020-05-26 08:44:47 +00:00
writer . Write ( FormattableString . Invariant ( $"{(curveData?.RepeatCount ?? 0) + 1}," ) ) ;
writer . Write ( FormattableString . Invariant ( $"{pathData.Path.Distance}," ) ) ;
2019-12-18 08:35:51 +00:00
2020-05-26 08:44:47 +00:00
if ( curveData ! = null )
2019-12-18 08:35:51 +00:00
{
2020-05-26 08:44:47 +00:00
for ( int i = 0 ; i < curveData . NodeSamples . Count ; i + + )
{
writer . Write ( FormattableString . Invariant ( $"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}" ) ) ;
writer . Write ( i ! = curveData . NodeSamples . Count - 1 ? "|" : "," ) ;
}
for ( int i = 0 ; i < curveData . NodeSamples . Count ; i + + )
{
writer . Write ( getSampleBank ( curveData . NodeSamples [ i ] , true ) ) ;
writer . Write ( i ! = curveData . NodeSamples . Count - 1 ? "|" : "," ) ;
}
2019-12-18 08:35:51 +00:00
}
2019-12-10 11:44:45 +00:00
}
2020-04-22 07:40:07 +00:00
private void addEndTimeData ( TextWriter writer , HitObject hitObject )
{
2020-05-27 03:38:39 +00:00
var endTimeData = ( IHasDuration ) hitObject ;
2020-04-22 07:40:07 +00:00
var type = getObjectType ( hitObject ) ;
char suffix = ',' ;
// Holds write the end time as if it's part of sample data.
if ( type = = LegacyHitObjectType . Hold )
suffix = ':' ;
writer . Write ( FormattableString . Invariant ( $"{endTimeData.EndTime}{suffix}" ) ) ;
}
2019-12-12 11:04:46 +00:00
private string getSampleBank ( IList < HitSampleInfo > samples , bool banksOnly = false , bool zeroBanks = false )
2019-12-10 11:44:45 +00:00
{
2019-12-16 08:03:58 +00:00
LegacySampleBank normalBank = toLegacySampleBank ( samples . SingleOrDefault ( s = > s . Name = = HitSampleInfo . HIT_NORMAL ) ? . Bank ) ;
2019-12-10 11:44:45 +00:00
LegacySampleBank addBank = toLegacySampleBank ( samples . FirstOrDefault ( s = > ! string . IsNullOrEmpty ( s . Name ) & & s . Name ! = HitSampleInfo . HIT_NORMAL ) ? . Bank ) ;
StringBuilder sb = new StringBuilder ( ) ;
2019-12-12 11:04:46 +00:00
sb . Append ( FormattableString . Invariant ( $"{(zeroBanks ? 0 : (int)normalBank)}:" ) ) ;
sb . Append ( FormattableString . Invariant ( $"{(zeroBanks ? 0 : (int)addBank)}" ) ) ;
2019-12-10 11:44:45 +00:00
if ( ! banksOnly )
{
2020-04-21 05:55:17 +00:00
string customSampleBank = toLegacyCustomSampleBank ( samples . FirstOrDefault ( s = > ! string . IsNullOrEmpty ( s . Name ) ) ) ;
2019-12-16 08:05:24 +00:00
string sampleFilename = samples . FirstOrDefault ( s = > string . IsNullOrEmpty ( s . Name ) ) ? . LookupNames . First ( ) ? ? string . Empty ;
int volume = samples . FirstOrDefault ( ) ? . Volume ? ? 100 ;
2020-11-01 17:49:11 +00:00
sb . Append ( ':' ) ;
2019-12-10 11:44:45 +00:00
sb . Append ( FormattableString . Invariant ( $"{customSampleBank}:" ) ) ;
sb . Append ( FormattableString . Invariant ( $"{volume}:" ) ) ;
sb . Append ( FormattableString . Invariant ( $"{sampleFilename}" ) ) ;
}
return sb . ToString ( ) ;
}
2019-12-12 10:53:30 +00:00
private LegacyHitSoundType toLegacyHitSoundType ( IList < HitSampleInfo > samples )
2019-12-10 11:44:45 +00:00
{
2019-12-12 10:53:30 +00:00
LegacyHitSoundType type = LegacyHitSoundType . None ;
2019-12-10 11:44:45 +00:00
2019-12-12 10:53:30 +00:00
foreach ( var sample in samples )
{
switch ( sample . Name )
{
case HitSampleInfo . HIT_WHISTLE :
type | = LegacyHitSoundType . Whistle ;
break ;
2019-12-10 11:44:45 +00:00
2019-12-12 10:53:30 +00:00
case HitSampleInfo . HIT_FINISH :
type | = LegacyHitSoundType . Finish ;
break ;
2019-12-10 11:44:45 +00:00
2019-12-12 10:53:30 +00:00
case HitSampleInfo . HIT_CLAP :
type | = LegacyHitSoundType . Clap ;
break ;
}
2019-12-10 11:44:45 +00:00
}
2019-12-12 10:53:30 +00:00
return type ;
2019-12-10 11:44:45 +00:00
}
private LegacySampleBank toLegacySampleBank ( string sampleBank )
{
2019-12-16 08:07:30 +00:00
switch ( sampleBank ? . ToLowerInvariant ( ) )
2019-12-10 11:44:45 +00:00
{
case "normal" :
return LegacySampleBank . Normal ;
case "soft" :
return LegacySampleBank . Soft ;
case "drum" :
return LegacySampleBank . Drum ;
default :
return LegacySampleBank . None ;
}
}
2020-04-21 05:55:17 +00:00
private string toLegacyCustomSampleBank ( HitSampleInfo hitSampleInfo )
{
if ( hitSampleInfo is ConvertHitObjectParser . LegacyHitSampleInfo legacy )
return legacy . CustomSampleBank . ToString ( CultureInfo . InvariantCulture ) ;
return "0" ;
}
2019-12-10 11:44:45 +00:00
}
}