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.
2022-06-17 15:37:17 +08:00
#nullable disable
2019-12-10 19:44:45 +08:00
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}" ) ) ;
2023-05-03 12:33:31 +08:00
writer . WriteLine ( FormattableString . Invariant (
$"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints?.FirstOrDefault() ?? 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
{
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
}
2023-05-01 22:53:38 +08:00
LegacyControlPointProperties lastControlPointProperties = new LegacyControlPointProperties ( ) ;
2023-05-01 20:56:29 +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 ( ) ;
2023-05-01 22:53:38 +08:00
var controlPointProperties = getLegacyControlPointProperties ( group , groupTimingPoint ! = null ) ;
2020-04-21 14:05:24 +08:00
// 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}," ) ) ;
2023-05-01 22:53:38 +08:00
outputControlPointAt ( controlPointProperties , true ) ;
lastControlPointProperties = controlPointProperties ;
lastControlPointProperties . SliderVelocity = 1 ;
2020-04-21 14:05:24 +08:00
}
2023-05-01 22:53:38 +08:00
if ( controlPointProperties . IsRedundant ( lastControlPointProperties ) )
continue ;
2020-04-21 14:05:24 +08:00
// Output any remaining effects as secondary non-timing control point.
writer . Write ( FormattableString . Invariant ( $"{group.Time}," ) ) ;
2023-05-01 22:53:38 +08:00
writer . Write ( FormattableString . Invariant ( $"{-100 / controlPointProperties.SliderVelocity}," ) ) ;
outputControlPointAt ( controlPointProperties , false ) ;
lastControlPointProperties = controlPointProperties ;
2020-04-21 14:05:24 +08:00
}
2019-12-10 19:44:45 +08:00
2023-05-01 22:53:38 +08:00
LegacyControlPointProperties getLegacyControlPointProperties ( ControlPointGroup group , bool updateSampleBank )
2020-04-21 14:05:24 +08:00
{
2023-05-03 12:33:31 +08:00
var timingPoint = legacyControlPoints . TimingPointAt ( group . Time ) ;
var difficultyPoint = legacyControlPoints . DifficultyPointAt ( group . Time ) ;
var samplePoint = legacyControlPoints . SamplePointAt ( group . Time ) ;
var effectPoint = legacyControlPoints . EffectPointAt ( group . 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 ) ) ;
2023-05-01 20:56:29 +08:00
int customSampleBank = toLegacyCustomSampleBank ( tempHitSample ) ;
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 ;
2023-02-28 18:29:31 +08:00
if ( timingPoint . OmitFirstBarLine )
2019-12-10 19:44:45 +08:00
effectFlags | = LegacyEffectFlags . OmitFirstBarLine ;
2023-05-01 22:53:38 +08:00
return new LegacyControlPointProperties
{
SliderVelocity = difficultyPoint . SliderVelocity ,
TimingSignature = timingPoint . TimeSignature . Numerator ,
SampleBank = updateSampleBank ? ( int ) toLegacySampleBank ( tempHitSample . Bank ) : lastControlPointProperties . SampleBank ,
// Inherit the previous custom sample bank if the current custom sample bank is not set
CustomSampleBank = customSampleBank > = 0 ? customSampleBank : lastControlPointProperties . CustomSampleBank ,
SampleVolume = tempHitSample . Volume ,
EffectFlags = effectFlags
} ;
}
void outputControlPointAt ( LegacyControlPointProperties controlPoint , bool isTimingPoint )
{
writer . Write ( FormattableString . Invariant ( $"{controlPoint.TimingSignature.ToString(CultureInfo.InvariantCulture)}," ) ) ;
writer . Write ( FormattableString . Invariant ( $"{controlPoint.SampleBank.ToString(CultureInfo.InvariantCulture)}," ) ) ;
writer . Write ( FormattableString . Invariant ( $"{controlPoint.CustomSampleBank.ToString(CultureInfo.InvariantCulture)}," ) ) ;
writer . Write ( FormattableString . Invariant ( $"{controlPoint.SampleVolume.ToString(CultureInfo.InvariantCulture)}," ) ) ;
writer . Write ( FormattableString . Invariant ( $"{(isTimingPoint ? " 1 " : " 0 ")}," ) ) ;
writer . Write ( FormattableString . Invariant ( $"{((int)controlPoint.EffectFlags).ToString(CultureInfo.InvariantCulture)}" ) ) ;
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 )
2023-04-26 23:55:38 +08:00
{
if ( hitObject is IHasSliderVelocity hasSliderVelocity )
yield return new DifficultyControlPoint { Time = hitObject . StartTime , SliderVelocity = hasSliderVelocity . SliderVelocity } ;
}
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 )
{
2023-04-25 21:53:36 +08:00
if ( hitObject . Samples . Count > 0 )
{
int volume = hitObject . Samples . Max ( o = > o . Volume ) ;
2023-04-26 20:28:48 +08:00
int customIndex = hitObject . Samples . Any ( o = > o is ConvertHitObjectParser . LegacyHitSampleInfo )
? hitObject . Samples . OfType < ConvertHitObjectParser . LegacyHitSampleInfo > ( ) . Max ( o = > o . CustomSampleBank )
2023-05-01 20:56:29 +08:00
: - 1 ;
2023-04-25 21:53:36 +08:00
yield return new LegacyBeatmapDecoder . LegacySampleControlPoint { Time = hitObject . GetEndTime ( ) , SampleVolume = volume , CustomSampleBank = customIndex } ;
}
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 ] ;
2023-01-03 18:41:08 +08:00
writer . Write ( FormattableString . Invariant ( $"Combo{1 + i}: " ) ) ;
2020-08-23 21:08:02 +08:00
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 )
{
2022-06-24 20:25:23 +08:00
case IHasPath :
2019-12-18 16:35:51 +08:00
type | = LegacyHitObjectType . Slider ;
break ;
2022-06-24 20:25:23 +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
{
2023-04-30 23:46:47 +08:00
for ( int i = 0 ; i < curveData . SpanCount ( ) + 1 ; i + + )
2020-05-26 16:44:47 +08:00
{
2023-04-30 23:46:47 +08:00
writer . Write ( FormattableString . Invariant ( $"{(i < curveData.NodeSamples.Count ? (int)toLegacyHitSoundType(curveData.NodeSamples[i]) : 0)}" ) ) ;
writer . Write ( i ! = curveData . SpanCount ( ) ? "|" : "," ) ;
2020-05-26 16:44:47 +08:00
}
2023-04-30 23:46:47 +08:00
for ( int i = 0 ; i < curveData . SpanCount ( ) + 1 ; i + + )
2020-05-26 16:44:47 +08:00
{
2023-04-30 23:46:47 +08:00
writer . Write ( i < curveData . NodeSamples . Count ? getSampleBank ( curveData . NodeSamples [ i ] , true ) : "0:0" ) ;
writer . Write ( i ! = curveData . SpanCount ( ) ? "|" : "," ) ;
2020-05-26 16:44:47 +08:00
}
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 )
{
2023-05-01 20:56:29 +08:00
int 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 ;
2023-05-01 18:31:27 +08:00
// We want to ignore custom sample banks and volume when not encoding to the mania game mode,
// because they cause unexpected results in the editor and are already satisfied by the control points.
if ( onlineRulesetID ! = 3 )
{
2023-05-01 20:56:29 +08:00
customSampleBank = 0 ;
2023-05-01 18:31:27 +08:00
volume = 0 ;
}
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
{
2022-10-19 19:34:41 +08:00
case HitSampleInfo . BANK_NORMAL :
2019-12-10 19:44:45 +08:00
return LegacySampleBank . Normal ;
2022-10-19 19:34:41 +08:00
case HitSampleInfo . BANK_SOFT :
2019-12-10 19:44:45 +08:00
return LegacySampleBank . Soft ;
2022-10-19 19:34:41 +08:00
case HitSampleInfo . BANK_DRUM :
2019-12-10 19:44:45 +08:00
return LegacySampleBank . Drum ;
default :
return LegacySampleBank . None ;
}
}
2023-05-01 20:56:29 +08:00
private int toLegacyCustomSampleBank ( HitSampleInfo hitSampleInfo )
2020-04-21 13:55:17 +08:00
{
if ( hitSampleInfo is ConvertHitObjectParser . LegacyHitSampleInfo legacy )
2023-05-01 20:56:29 +08:00
return legacy . CustomSampleBank ;
2020-04-21 13:55:17 +08:00
2023-05-01 20:56:29 +08:00
return 0 ;
2020-04-21 13:55:17 +08:00
}
2023-05-03 12:33:31 +08:00
private struct LegacyControlPointProperties
{
internal double SliderVelocity { get ; set ; }
internal int TimingSignature { get ; init ; }
internal int SampleBank { get ; init ; }
internal int CustomSampleBank { get ; init ; }
internal int SampleVolume { get ; init ; }
internal LegacyEffectFlags EffectFlags { get ; init ; }
internal bool IsRedundant ( LegacyControlPointProperties other ) = >
SliderVelocity = = other . SliderVelocity & &
TimingSignature = = other . TimingSignature & &
SampleBank = = other . SampleBank & &
CustomSampleBank = = other . CustomSampleBank & &
SampleVolume = = other . SampleVolume & &
EffectFlags = = other . EffectFlags ;
2020-04-21 13:55:17 +08:00
}
2019-12-10 19:44:45 +08:00
}
}