2019-12-10 19:44:45 +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.
using System ;
using System.Collections.Generic ;
2020-04-21 13:55:17 +08:00
using System.Globalization ;
2019-12-10 19:44:45 +08:00
using System.IO ;
using System.Linq ;
using System.Text ;
2020-08-12 12:37:25 +08:00
using JetBrains.Annotations ;
2019-12-10 19:44:45 +08:00
using osu.Game.Audio ;
using osu.Game.Beatmaps.ControlPoints ;
using osu.Game.Beatmaps.Legacy ;
using osu.Game.Rulesets.Objects ;
2020-04-21 13:55:17 +08:00
using osu.Game.Rulesets.Objects.Legacy ;
2019-12-10 19:44:45 +08:00
using osu.Game.Rulesets.Objects.Types ;
2020-08-10 11:21:10 +08:00
using osu.Game.Skinning ;
2020-04-21 15:04:58 +08:00
using osuTK ;
2020-08-23 21:08:02 +08:00
using osuTK.Graphics ;
2019-12-10 19:44:45 +08:00
namespace osu.Game.Beatmaps.Formats
{
public class LegacyBeatmapEncoder
{
2022-05-18 16:11:08 +08:00
public const int FIRST_LAZER_VERSION = 128 ;
2019-12-10 19:44:45 +08:00
2021-08-30 13:40:25 +08:00
/// <summary>
/// osu! is generally slower than taiko, so a factor is added to increase
/// speed. This must be used everywhere slider length or beat length is used.
/// </summary>
public const float LEGACY_TAIKO_VELOCITY_MULTIPLIER = 1.4f ;
2019-12-10 19:44:45 +08:00
private readonly IBeatmap beatmap ;
2020-08-31 23:24:03 +08:00
[CanBeNull]
2020-09-01 23:58:06 +08:00
private readonly ISkin skin ;
2020-08-23 21:08:02 +08:00
2022-01-27 14:19:48 +08:00
private readonly int onlineRulesetID ;
2020-08-23 21:08:02 +08:00
/// <summary>
/// Creates a new <see cref="LegacyBeatmapEncoder"/>.
/// </summary>
/// <param name="beatmap">The beatmap to encode.</param>
2020-08-30 22:07:58 +08:00
/// <param name="skin">The beatmap's skin, used for encoding combo colours.</param>
2020-09-01 23:58:06 +08:00
public LegacyBeatmapEncoder ( IBeatmap beatmap , [ CanBeNull ] ISkin skin )
2019-12-10 19:44:45 +08:00
{
this . beatmap = beatmap ;
2020-08-16 04:03:24 +08:00
this . skin = skin ;
2019-12-10 19:44:45 +08:00
2022-01-27 14:19:48 +08:00
onlineRulesetID = beatmap . BeatmapInfo . Ruleset . OnlineID ;
if ( onlineRulesetID < 0 | | onlineRulesetID > 3 )
2019-12-10 19:44:45 +08:00
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 )
{
2022-05-18 16:11:08 +08:00
writer . WriteLine ( $"osu file format v{FIRST_LAZER_VERSION}" ) ;
2019-12-10 19:44:45 +08:00
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 14:05:24 +08:00
handleControlPoints ( writer ) ;
2019-12-10 19:44:45 +08:00
2020-08-10 11:21:10 +08:00
writer . WriteLine ( ) ;
2020-08-31 23:24:03 +08:00
handleColours ( writer ) ;
2020-08-10 11:21:10 +08:00
2019-12-10 19:44:45 +08:00
writer . WriteLine ( ) ;
handleHitObjects ( writer ) ;
}
private void handleGeneral ( TextWriter writer )
{
writer . WriteLine ( "[General]" ) ;
2021-11-04 12:59:40 +08:00
if ( ! string . IsNullOrEmpty ( beatmap . Metadata . AudioFile ) ) writer . WriteLine ( FormattableString . Invariant ( $"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}" ) ) ;
2019-12-10 19:44:45 +08:00
writer . WriteLine ( FormattableString . Invariant ( $"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"PreviewTime: {beatmap.Metadata.PreviewTime}" ) ) ;
2021-08-25 02:53:27 +08:00
writer . WriteLine ( FormattableString . Invariant ( $"Countdown: {(int)beatmap.BeatmapInfo.Countdown}" ) ) ;
2021-08-25 17:00:57 +08:00
writer . WriteLine ( FormattableString . Invariant ( $"SampleSet: {toLegacySampleBank((beatmap.HitObjects.FirstOrDefault()?.SampleControlPoint ?? SampleControlPoint.DEFAULT).SampleBank)}" ) ) ;
2019-12-10 19:44:45 +08:00
writer . WriteLine ( FormattableString . Invariant ( $"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}" ) ) ;
2022-01-27 14:19:48 +08:00
writer . WriteLine ( FormattableString . Invariant ( $"Mode: {onlineRulesetID}" ) ) ;
2019-12-16 16:06:52 +08:00
writer . WriteLine ( FormattableString . Invariant ( $"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}" ) ) ;
2019-12-10 19:44:45 +08: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);
2021-08-22 20:28:16 +08:00
if ( beatmap . BeatmapInfo . EpilepsyWarning )
writer . WriteLine ( @"EpilepsyWarning: 1" ) ;
2021-08-25 02:53:27 +08:00
if ( beatmap . BeatmapInfo . CountdownOffset > 0 )
writer . WriteLine ( FormattableString . Invariant ( $@"CountdownOffset: {beatmap.BeatmapInfo.CountdownOffset}" ) ) ;
2022-01-27 14:19:48 +08:00
if ( onlineRulesetID = = 3 )
2019-12-16 16:06:52 +08:00
writer . WriteLine ( FormattableString . Invariant ( $"SpecialStyle: {(beatmap.BeatmapInfo.SpecialStyle ? '1' : '0')}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"WidescreenStoryboard: {(beatmap.BeatmapInfo.WidescreenStoryboard ? '1' : '0')}" ) ) ;
2021-09-12 22:47:38 +08:00
if ( beatmap . BeatmapInfo . SamplesMatchPlaybackRate )
writer . WriteLine ( @"SamplesMatchPlaybackRate: 1" ) ;
2019-12-10 19:44:45 +08:00
}
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}" ) ) ;
2021-11-04 12:59:40 +08:00
if ( ! string . IsNullOrEmpty ( beatmap . Metadata . TitleUnicode ) ) writer . WriteLine ( FormattableString . Invariant ( $"TitleUnicode: {beatmap.Metadata.TitleUnicode}" ) ) ;
2019-12-10 19:44:45 +08:00
writer . WriteLine ( FormattableString . Invariant ( $"Artist: {beatmap.Metadata.Artist}" ) ) ;
2021-11-04 12:59:40 +08:00
if ( ! string . IsNullOrEmpty ( beatmap . Metadata . ArtistUnicode ) ) writer . WriteLine ( FormattableString . Invariant ( $"ArtistUnicode: {beatmap.Metadata.ArtistUnicode}" ) ) ;
2021-11-04 17:46:26 +08:00
writer . WriteLine ( FormattableString . Invariant ( $"Creator: {beatmap.Metadata.Author.Username}" ) ) ;
2021-11-11 16:19:53 +08:00
writer . WriteLine ( FormattableString . Invariant ( $"Version: {beatmap.BeatmapInfo.DifficultyName}" ) ) ;
2021-11-04 12:59:40 +08:00
if ( ! string . IsNullOrEmpty ( beatmap . Metadata . Source ) ) writer . WriteLine ( FormattableString . Invariant ( $"Source: {beatmap.Metadata.Source}" ) ) ;
if ( ! string . IsNullOrEmpty ( beatmap . Metadata . Tags ) ) writer . WriteLine ( FormattableString . Invariant ( $"Tags: {beatmap.Metadata.Tags}" ) ) ;
2021-11-22 13:55:41 +08:00
if ( beatmap . BeatmapInfo . OnlineID > 0 ) writer . WriteLine ( FormattableString . Invariant ( $"BeatmapID: {beatmap.BeatmapInfo.OnlineID}" ) ) ;
if ( beatmap . BeatmapInfo . BeatmapSet ? . OnlineID > 0 ) writer . WriteLine ( FormattableString . Invariant ( $"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineID}" ) ) ;
2019-12-10 19:44:45 +08:00
}
private void handleDifficulty ( TextWriter writer )
{
writer . WriteLine ( "[Difficulty]" ) ;
2021-10-02 11:34:29 +08:00
writer . WriteLine ( FormattableString . Invariant ( $"HPDrainRate: {beatmap.Difficulty.DrainRate}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"CircleSize: {beatmap.Difficulty.CircleSize}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"OverallDifficulty: {beatmap.Difficulty.OverallDifficulty}" ) ) ;
writer . WriteLine ( FormattableString . Invariant ( $"ApproachRate: {beatmap.Difficulty.ApproachRate}" ) ) ;
2020-04-21 15:45:01 +08:00
2021-08-30 15:27:24 +08:00
// Taiko adjusts the slider multiplier (see: LEGACY_TAIKO_VELOCITY_MULTIPLIER)
2022-01-27 14:19:48 +08:00
writer . WriteLine ( onlineRulesetID = = 1
2021-10-02 11:34:29 +08:00
? FormattableString . Invariant ( $"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier / LEGACY_TAIKO_VELOCITY_MULTIPLIER}" )
: FormattableString . Invariant ( $"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier}" ) ) ;
2020-04-21 15:45:01 +08:00
2021-10-02 11:34:29 +08:00
writer . WriteLine ( FormattableString . Invariant ( $"SliderTickRate: {beatmap.Difficulty.SliderTickRate}" ) ) ;
2019-12-10 19:44:45 +08:00
}
private void handleEvents ( TextWriter writer )
{
2019-12-12 17:48:22 +08:00
writer . WriteLine ( "[Events]" ) ;
2019-12-12 17:49:47 +08:00
if ( ! string . IsNullOrEmpty ( beatmap . BeatmapInfo . Metadata . BackgroundFile ) )
2019-12-12 17:51:05 +08:00
writer . WriteLine ( FormattableString . Invariant ( $"{(int)LegacyEventType.Background},0,\" { beatmap . BeatmapInfo . Metadata . BackgroundFile } \ ",0,0" ) ) ;
2019-12-12 17:49:47 +08:00
2019-12-12 17:47:28 +08:00
foreach ( var b in beatmap . Breaks )
writer . WriteLine ( FormattableString . Invariant ( $"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}" ) ) ;
2019-12-10 19:44:45 +08:00
}
2020-04-21 14:05:24 +08:00
private void handleControlPoints ( TextWriter writer )
2019-12-10 19:44:45 +08:00
{
if ( beatmap . ControlPointInfo . Groups . Count = = 0 )
return ;
2021-09-06 20:05:43 +08:00
var legacyControlPoints = new LegacyControlPointInfo ( ) ;
foreach ( var point in beatmap . ControlPointInfo . AllControlPoints )
legacyControlPoints . Add ( point . Time , point . DeepClone ( ) ) ;
2021-08-30 15:58:21 +08:00
2021-09-06 20:05:43 +08:00
writer . WriteLine ( "[TimingPoints]" ) ;
2021-08-30 15:58:21 +08:00
2021-09-06 20:05:43 +08:00
SampleControlPoint lastRelevantSamplePoint = null ;
DifficultyControlPoint lastRelevantDifficultyPoint = null ;
2021-08-30 15:58:21 +08:00
2022-05-08 12:45:21 +08:00
// In osu!taiko and osu!mania, a scroll speed is stored as "slider velocity" in legacy formats.
// In that case, a scrolling speed change is a global effect and per-hit object difficulty control points are ignored.
bool scrollSpeedEncodedAsSliderVelocity = onlineRulesetID = = 1 | | onlineRulesetID = = 3 ;
2021-08-30 15:58:21 +08:00
2021-09-06 20:05:43 +08:00
// iterate over hitobjects and pull out all required sample and difficulty changes
2021-09-17 00:04:26 +08:00
extractDifficultyControlPoints ( beatmap . HitObjects ) ;
extractSampleControlPoints ( beatmap . HitObjects ) ;
2021-09-01 17:19:25 +08:00
2022-05-08 12:45:21 +08:00
if ( scrollSpeedEncodedAsSliderVelocity )
2021-09-06 20:05:43 +08:00
{
foreach ( var point in legacyControlPoints . EffectPoints )
legacyControlPoints . Add ( point . Time , new DifficultyControlPoint { SliderVelocity = point . ScrollSpeed } ) ;
2021-08-30 15:58:21 +08:00
}
2021-09-06 20:05:43 +08:00
foreach ( var group in legacyControlPoints . Groups )
2019-12-10 19:44:45 +08:00
{
2020-04-21 14:05:24 +08: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}," ) ) ;
2021-08-30 15:58:21 +08:00
outputControlPointAt ( groupTimingPoint . Time , true ) ;
2020-04-21 14:05:24 +08:00
}
// Output any remaining effects as secondary non-timing control point.
2021-09-06 20:05:43 +08:00
var difficultyPoint = legacyControlPoints . DifficultyPointAt ( group . Time ) ;
2020-04-21 14:05:24 +08:00
writer . Write ( FormattableString . Invariant ( $"{group.Time}," ) ) ;
2021-08-31 22:59:36 +08:00
writer . Write ( FormattableString . Invariant ( $"{-100 / difficultyPoint.SliderVelocity}," ) ) ;
2021-08-30 15:58:21 +08:00
outputControlPointAt ( group . Time , false ) ;
2020-04-21 14:05:24 +08:00
}
2019-12-10 19:44:45 +08:00
2021-08-30 15:58:21 +08:00
void outputControlPointAt ( double time , bool isTimingPoint )
2020-04-21 14:05:24 +08:00
{
2021-09-06 20:05:43 +08:00
var samplePoint = legacyControlPoints . SamplePointAt ( time ) ;
var effectPoint = legacyControlPoints . EffectPointAt ( time ) ;
2019-12-10 19:44:45 +08:00
// Apply the control point to a hit sample to uncover legacy properties (e.g. suffix)
2020-12-01 14:37:51 +08:00
HitSampleInfo tempHitSample = samplePoint . ApplyTo ( new ConvertHitObjectParser . LegacyHitSampleInfo ( string . Empty ) ) ;
2019-12-10 19:44:45 +08:00
// Convert effect flags to the legacy format
LegacyEffectFlags effectFlags = LegacyEffectFlags . None ;
if ( effectPoint . KiaiMode )
effectFlags | = LegacyEffectFlags . Kiai ;
if ( effectPoint . OmitFirstBarLine )
effectFlags | = LegacyEffectFlags . OmitFirstBarLine ;
2022-01-23 00:27:27 +08:00
writer . Write ( FormattableString . Invariant ( $"{legacyControlPoints.TimingPointAt(time).TimeSignature.Numerator}," ) ) ;
2019-12-10 19:44:45 +08:00
writer . Write ( FormattableString . Invariant ( $"{(int)toLegacySampleBank(tempHitSample.Bank)}," ) ) ;
2020-04-21 13:55:17 +08:00
writer . Write ( FormattableString . Invariant ( $"{toLegacyCustomSampleBank(tempHitSample)}," ) ) ;
2019-12-10 19:44:45 +08:00
writer . Write ( FormattableString . Invariant ( $"{tempHitSample.Volume}," ) ) ;
2020-04-21 14:05:24 +08:00
writer . Write ( FormattableString . Invariant ( $"{(isTimingPoint ? '1' : '0')}," ) ) ;
2019-12-10 19:44:45 +08:00
writer . Write ( FormattableString . Invariant ( $"{(int)effectFlags}" ) ) ;
2019-12-16 16:08:46 +08:00
writer . WriteLine ( ) ;
2019-12-10 19:44:45 +08:00
}
2021-09-10 15:51:10 +08:00
2021-09-17 00:04:26 +08:00
IEnumerable < DifficultyControlPoint > collectDifficultyControlPoints ( IEnumerable < HitObject > hitObjects )
2021-09-10 15:51:10 +08:00
{
2022-05-08 12:45:21 +08:00
if ( scrollSpeedEncodedAsSliderVelocity )
2021-09-17 00:04:26 +08:00
yield break ;
2021-09-10 15:51:10 +08:00
2021-09-17 00:04:26 +08:00
foreach ( var hitObject in hitObjects )
yield return hitObject . DifficultyControlPoint ;
2021-09-10 15:51:10 +08:00
}
2021-09-17 00:04:26 +08:00
void extractDifficultyControlPoints ( IEnumerable < HitObject > hitObjects )
2021-09-10 15:51:10 +08:00
{
2021-09-17 00:04:26 +08:00
foreach ( var hDifficultyPoint in collectDifficultyControlPoints ( hitObjects ) . OrderBy ( dp = > dp . Time ) )
{
if ( ! hDifficultyPoint . IsRedundant ( lastRelevantDifficultyPoint ) )
{
legacyControlPoints . Add ( hDifficultyPoint . Time , hDifficultyPoint ) ;
lastRelevantDifficultyPoint = hDifficultyPoint ;
}
}
}
2021-09-10 15:51:10 +08:00
2021-09-17 00:04:26 +08:00
IEnumerable < SampleControlPoint > collectSampleControlPoints ( IEnumerable < HitObject > hitObjects )
{
foreach ( var hitObject in hitObjects )
{
yield return hitObject . SampleControlPoint ;
2021-09-10 15:51:10 +08:00
2021-09-17 00:04:26 +08:00
foreach ( var nested in collectSampleControlPoints ( hitObject . NestedHitObjects ) )
yield return nested ;
}
}
2021-09-10 15:51:10 +08:00
2021-09-17 00:04:26 +08:00
void extractSampleControlPoints ( IEnumerable < HitObject > hitObject )
{
foreach ( var hSamplePoint in collectSampleControlPoints ( hitObject ) . OrderBy ( sp = > sp . Time ) )
2021-09-10 15:51:10 +08:00
{
2021-09-17 00:04:26 +08:00
if ( ! hSamplePoint . IsRedundant ( lastRelevantSamplePoint ) )
{
legacyControlPoints . Add ( hSamplePoint . Time , hSamplePoint ) ;
lastRelevantSamplePoint = hSamplePoint ;
}
2021-09-10 15:51:10 +08:00
}
}
2019-12-10 19:44:45 +08:00
}
2020-08-31 23:24:03 +08:00
private void handleColours ( TextWriter writer )
2020-08-10 11:21:10 +08:00
{
2020-08-31 23:24:03 +08:00
var colours = skin ? . GetConfig < GlobalSkinColours , IReadOnlyList < Color4 > > ( GlobalSkinColours . ComboColours ) ? . Value ;
2020-08-10 11:21:10 +08:00
2020-08-12 12:37:33 +08:00
if ( colours = = null | | colours . Count = = 0 )
2020-08-10 11:21:10 +08:00
return ;
writer . WriteLine ( "[Colours]" ) ;
2021-10-27 12:04:41 +08:00
for ( int i = 0 ; i < colours . Count ; i + + )
2020-08-10 11:21:10 +08:00
{
var comboColour = colours [ i ] ;
2020-08-23 21:08:02 +08: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 22:11:49 +08:00
writer . Write ( FormattableString . Invariant ( $"{(byte)(comboColour.A * byte.MaxValue)}" ) ) ;
writer . WriteLine ( ) ;
2020-08-10 11:21:10 +08:00
}
}
2019-12-10 19:44:45 +08:00
private void handleHitObjects ( TextWriter writer )
{
2020-11-07 23:17:23 +08:00
writer . WriteLine ( "[HitObjects]" ) ;
2019-12-10 19:44:45 +08:00
if ( beatmap . HitObjects . Count = = 0 )
return ;
2020-04-21 15:04:58 +08:00
foreach ( var h in beatmap . HitObjects )
handleHitObject ( writer , h ) ;
}
private void handleHitObject ( TextWriter writer , HitObject hitObject )
{
Vector2 position = new Vector2 ( 256 , 192 ) ;
2022-01-27 14:19:48 +08:00
switch ( onlineRulesetID )
2019-12-10 19:44:45 +08:00
{
2019-12-16 15:57:40 +08:00
case 0 :
2020-04-21 15:04:58 +08:00
case 2 :
2021-07-14 13:38:38 +08:00
position = ( ( IHasPosition ) hitObject ) . Position ;
2020-04-21 15:04:58 +08:00
break ;
case 3 :
2021-10-02 11:34:29 +08:00
int totalColumns = ( int ) Math . Max ( 1 , beatmap . Difficulty . CircleSize ) ;
2020-04-21 15:04:58 +08:00
position . X = ( int ) Math . Ceiling ( ( ( IHasXPosition ) hitObject ) . X * ( 512f / totalColumns ) ) ;
break ;
}
2019-12-10 19:44:45 +08:00
2020-04-21 15:04:58 +08:00
writer . Write ( FormattableString . Invariant ( $"{position.X}," ) ) ;
writer . Write ( FormattableString . Invariant ( $"{position.Y}," ) ) ;
2019-12-10 19:44:45 +08:00
writer . Write ( FormattableString . Invariant ( $"{hitObject.StartTime}," ) ) ;
2019-12-18 16:35:51 +08:00
writer . Write ( FormattableString . Invariant ( $"{(int)getObjectType(hitObject)}," ) ) ;
2020-04-21 15:04:58 +08:00
writer . Write ( FormattableString . Invariant ( $"{(int)toLegacyHitSoundType(hitObject.Samples)}," ) ) ;
2019-12-10 19:44:45 +08:00
2020-05-31 21:30:55 +08:00
if ( hitObject is IHasPath path )
2019-12-10 19:44:45 +08:00
{
2020-05-31 21:30:55 +08:00
addPathData ( writer , path , position ) ;
2021-04-09 14:28:42 +08:00
writer . Write ( getSampleBank ( hitObject . Samples ) ) ;
2019-12-18 16:35:51 +08:00
}
else
{
2020-05-27 11:38:39 +08:00
if ( hitObject is IHasDuration )
2020-04-22 15:40:07 +08:00
addEndTimeData ( writer , hitObject ) ;
2019-12-18 16:35:51 +08:00
writer . Write ( getSampleBank ( hitObject . Samples ) ) ;
}
2019-12-12 18:52:15 +08:00
2019-12-18 16:35:51 +08:00
writer . WriteLine ( ) ;
}
2020-04-22 15:27:07 +08:00
private LegacyHitObjectType getObjectType ( HitObject hitObject )
2019-12-18 16:35:51 +08:00
{
2020-04-21 15:04:58 +08:00
LegacyHitObjectType type = 0 ;
2019-12-18 16:35:51 +08:00
2020-04-21 15:04:58 +08:00
if ( hitObject is IHasCombo combo )
{
type = ( LegacyHitObjectType ) ( combo . ComboOffset < < 4 ) ;
2019-12-18 16:35:51 +08:00
2020-04-21 15:04:58 +08:00
if ( combo . NewCombo )
type | = LegacyHitObjectType . NewCombo ;
}
2019-12-10 19:44:45 +08:00
2019-12-18 16:35:51 +08:00
switch ( hitObject )
{
2020-05-26 16:44:47 +08:00
case IHasPath _ :
2019-12-18 16:35:51 +08:00
type | = LegacyHitObjectType . Slider ;
break ;
2020-05-27 11:38:39 +08:00
case IHasDuration _ :
2022-01-27 14:19:48 +08:00
if ( onlineRulesetID = = 3 )
2020-04-22 15:27:07 +08:00
type | = LegacyHitObjectType . Hold ;
else
type | = LegacyHitObjectType . Spinner ;
2019-12-18 16:35:51 +08:00
break ;
default :
type | = LegacyHitObjectType . Circle ;
break ;
}
return type ;
}
2020-05-26 16:44:47 +08:00
private void addPathData ( TextWriter writer , IHasPath pathData , Vector2 position )
2019-12-18 16:35:51 +08:00
{
PathType ? lastType = null ;
2020-05-26 16:44:47 +08:00
for ( int i = 0 ; i < pathData . Path . ControlPoints . Count ; i + + )
2019-12-18 16:35:51 +08:00
{
2020-05-26 16:44:47 +08:00
PathControlPoint point = pathData . Path . ControlPoints [ i ] ;
2019-12-18 16:35:51 +08:00
2021-08-26 00:42:57 +08:00
if ( point . Type ! = null )
2019-12-18 16:35:51 +08:00
{
2021-04-06 13:10:59 +08:00
// We've reached a new (explicit) segment!
2021-04-05 18:59:54 +08:00
2021-04-06 13:10:59 +08: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.
2021-04-05 23:21:45 +08:00
// One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments
2021-08-26 00:42:57 +08:00
bool needsExplicitSegment = point . Type ! = lastType | | point . Type = = PathType . PerfectCurve ;
2021-04-05 18:59:54 +08:00
2021-04-06 00:01:16 +08:00
// Another 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.
2021-04-06 13:10:59 +08:00
// 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.
2021-08-26 00:42:57 +08:00
Vector2 p1 = position + pathData . Path . ControlPoints [ i - 1 ] . Position ;
Vector2 p2 = position + pathData . Path . ControlPoints [ i - 2 ] . Position ;
2021-04-06 13:10:59 +08:00
if ( ( int ) p1 . X = = ( int ) p2 . X & & ( int ) p1 . Y = = ( int ) p2 . Y )
needsExplicitSegment = true ;
}
if ( needsExplicitSegment )
2019-12-10 19:44:45 +08:00
{
2021-08-26 00:42:57 +08:00
switch ( point . Type )
2019-12-12 18:52:15 +08:00
{
2019-12-18 16:35:51 +08: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 18:52:15 +08:00
}
2019-12-10 19:44:45 +08:00
2021-08-26 00:42:57 +08:00
lastType = point . Type ;
2019-12-18 16:35:51 +08:00
}
else
2019-12-12 18:01:15 +08:00
{
2019-12-18 16:35:51 +08:00
// New segment with the same type - duplicate the control point
2021-08-26 00:42:57 +08:00
writer . Write ( FormattableString . Invariant ( $"{position.X + point.Position.X}:{position.Y + point.Position.Y}|" ) ) ;
2019-12-12 18:01:15 +08:00
}
2019-12-10 19:44:45 +08:00
}
2019-12-18 16:35:51 +08:00
if ( i ! = 0 )
2019-12-10 19:44:45 +08:00
{
2021-08-26 00:42:57 +08:00
writer . Write ( FormattableString . Invariant ( $"{position.X + point.Position.X}:{position.Y + point.Position.Y}" ) ) ;
2020-05-26 16:44:47 +08:00
writer . Write ( i ! = pathData . Path . ControlPoints . Count - 1 ? "|" : "," ) ;
2019-12-10 19:44:45 +08:00
}
}
2020-05-26 16:44:47 +08:00
var curveData = pathData as IHasPathWithRepeats ;
2019-12-12 19:04:46 +08:00
2020-05-26 16:44:47 +08:00
writer . Write ( FormattableString . Invariant ( $"{(curveData?.RepeatCount ?? 0) + 1}," ) ) ;
2021-10-26 16:19:29 +08:00
writer . Write ( FormattableString . Invariant ( $"{pathData.Path.ExpectedDistance.Value ?? pathData.Path.Distance}," ) ) ;
2019-12-18 16:35:51 +08:00
2020-05-26 16:44:47 +08:00
if ( curveData ! = null )
2019-12-18 16:35:51 +08:00
{
2020-05-26 16:44:47 +08: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 16:35:51 +08:00
}
2019-12-10 19:44:45 +08:00
}
2020-04-22 15:40:07 +08:00
private void addEndTimeData ( TextWriter writer , HitObject hitObject )
{
2020-05-27 11:38:39 +08:00
var endTimeData = ( IHasDuration ) hitObject ;
2020-04-22 15:40:07 +08: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}" ) ) ;
}
2021-04-09 14:28:42 +08:00
private string getSampleBank ( IList < HitSampleInfo > samples , bool banksOnly = false )
2019-12-10 19:44:45 +08:00
{
2019-12-16 16:03:58 +08:00
LegacySampleBank normalBank = toLegacySampleBank ( samples . SingleOrDefault ( s = > s . Name = = HitSampleInfo . HIT_NORMAL ) ? . Bank ) ;
2019-12-10 19:44:45 +08:00
LegacySampleBank addBank = toLegacySampleBank ( samples . FirstOrDefault ( s = > ! string . IsNullOrEmpty ( s . Name ) & & s . Name ! = HitSampleInfo . HIT_NORMAL ) ? . Bank ) ;
StringBuilder sb = new StringBuilder ( ) ;
2021-04-09 14:28:42 +08:00
sb . Append ( FormattableString . Invariant ( $"{(int)normalBank}:" ) ) ;
sb . Append ( FormattableString . Invariant ( $"{(int)addBank}" ) ) ;
2019-12-10 19:44:45 +08:00
if ( ! banksOnly )
{
2020-04-21 13:55:17 +08:00
string customSampleBank = toLegacyCustomSampleBank ( samples . FirstOrDefault ( s = > ! string . IsNullOrEmpty ( s . Name ) ) ) ;
2019-12-16 16:05:24 +08:00
string sampleFilename = samples . FirstOrDefault ( s = > string . IsNullOrEmpty ( s . Name ) ) ? . LookupNames . First ( ) ? ? string . Empty ;
int volume = samples . FirstOrDefault ( ) ? . Volume ? ? 100 ;
2020-11-02 01:49:11 +08:00
sb . Append ( ':' ) ;
2019-12-10 19:44:45 +08:00
sb . Append ( FormattableString . Invariant ( $"{customSampleBank}:" ) ) ;
sb . Append ( FormattableString . Invariant ( $"{volume}:" ) ) ;
sb . Append ( FormattableString . Invariant ( $"{sampleFilename}" ) ) ;
}
return sb . ToString ( ) ;
}
2019-12-12 18:53:30 +08:00
private LegacyHitSoundType toLegacyHitSoundType ( IList < HitSampleInfo > samples )
2019-12-10 19:44:45 +08:00
{
2019-12-12 18:53:30 +08:00
LegacyHitSoundType type = LegacyHitSoundType . None ;
2019-12-10 19:44:45 +08:00
2019-12-12 18:53:30 +08:00
foreach ( var sample in samples )
{
switch ( sample . Name )
{
case HitSampleInfo . HIT_WHISTLE :
type | = LegacyHitSoundType . Whistle ;
break ;
2019-12-10 19:44:45 +08:00
2019-12-12 18:53:30 +08:00
case HitSampleInfo . HIT_FINISH :
type | = LegacyHitSoundType . Finish ;
break ;
2019-12-10 19:44:45 +08:00
2019-12-12 18:53:30 +08:00
case HitSampleInfo . HIT_CLAP :
type | = LegacyHitSoundType . Clap ;
break ;
}
2019-12-10 19:44:45 +08:00
}
2019-12-12 18:53:30 +08:00
return type ;
2019-12-10 19:44:45 +08:00
}
private LegacySampleBank toLegacySampleBank ( string sampleBank )
{
2019-12-16 16:07:30 +08:00
switch ( sampleBank ? . ToLowerInvariant ( ) )
2019-12-10 19:44:45 +08:00
{
case "normal" :
return LegacySampleBank . Normal ;
case "soft" :
return LegacySampleBank . Soft ;
case "drum" :
return LegacySampleBank . Drum ;
default :
return LegacySampleBank . None ;
}
}
2020-04-21 13:55:17 +08:00
private string toLegacyCustomSampleBank ( HitSampleInfo hitSampleInfo )
{
if ( hitSampleInfo is ConvertHitObjectParser . LegacyHitSampleInfo legacy )
return legacy . CustomSampleBank . ToString ( CultureInfo . InvariantCulture ) ;
return "0" ;
}
2019-12-10 19:44:45 +08:00
}
}