2019-01-24 17:43:03 +09: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 18:19:50 +09:00
2017-12-01 17:43:33 +01:00
using System ;
using System.Collections.Generic ;
2020-01-23 16:23:53 +01:00
using osu.Framework.Extensions ;
2018-03-22 12:58:02 +09:00
using osu.Framework.Logging ;
2018-06-28 18:20:43 +09:00
using osu.Game.Audio ;
using osu.Game.Beatmaps.ControlPoints ;
2019-09-10 00:43:30 +02:00
using osu.Game.IO ;
2020-04-14 21:05:07 +09:00
using osu.Game.Rulesets.Objects.Legacy ;
2018-11-20 16:51:59 +09:00
using osuTK.Graphics ;
2018-04-13 18:19:50 +09:00
2017-12-01 17:43:33 +01:00
namespace osu.Game.Beatmaps.Formats
{
2018-03-09 21:23:03 +09:00
public abstract class LegacyDecoder < T > : Decoder < T >
where T : new ( )
2017-12-01 17:43:33 +01:00
{
2020-11-12 17:03:42 +09:00
public const int LATEST_VERSION = 14 ;
2018-03-09 21:23:03 +09:00
protected readonly int FormatVersion ;
2018-04-13 18:19:50 +09:00
2018-03-09 21:23:03 +09:00
protected LegacyDecoder ( int version )
2017-12-01 17:43:33 +01:00
{
2018-03-09 21:23:03 +09:00
FormatVersion = version ;
2017-12-01 17:43:33 +01:00
}
2018-04-13 18:19:50 +09:00
2019-09-10 00:43:30 +02:00
protected override void ParseStreamInto ( LineBufferedReader stream , T output )
2017-12-01 17:43:33 +01:00
{
2021-11-02 13:17:21 +09:00
Section section = Section . General ;
2018-04-13 18:19:50 +09:00
2022-07-06 14:29:55 +09:00
string? line ;
2019-04-01 12:16:05 +09:00
2017-12-01 17:43:33 +01:00
while ( ( line = stream . ReadLine ( ) ) ! = null )
{
2017-12-02 16:05:39 +01:00
if ( ShouldSkipLine ( line ) )
2017-12-01 17:43:33 +01:00
continue ;
2018-04-13 18:19:50 +09:00
2021-03-25 13:35:54 +09:00
if ( section ! = Section . Metadata )
{
// comments should not be stripped from metadata lines, as the song metadata may contain "//" as valid data.
line = StripComments ( line ) ;
}
line = line . TrimEnd ( ) ;
2021-03-18 15:36:07 +09:00
2020-02-08 18:05:27 +01:00
if ( line . StartsWith ( '[' ) & & line . EndsWith ( ']' ) )
2017-12-01 17:43:33 +01:00
{
2019-12-14 20:54:22 +08:00
if ( ! Enum . TryParse ( line [ 1. . ^ 1 ] , out section ) )
2019-08-12 01:42:05 +09:00
Logger . Log ( $"Unknown section \" { line } \ " in \"{output}\"" ) ;
2018-04-13 18:19:50 +09:00
2020-03-30 17:18:09 +09:00
OnBeginNewSection ( section ) ;
2017-12-01 17:43:33 +01:00
continue ;
}
2018-04-13 18:19:50 +09:00
2019-08-07 19:33:54 +09:00
try
{
ParseLine ( output , section , line ) ;
}
catch ( Exception e )
{
2022-05-27 19:18:37 +09:00
Logger . Log ( $"Failed to process line \" { line } \ " into \"{output}\": {e.Message}" ) ;
2019-08-07 19:33:54 +09:00
}
2017-12-01 17:43:33 +01:00
}
}
2018-04-13 18:19:50 +09:00
2019-05-14 16:16:55 +09:00
protected virtual bool ShouldSkipLine ( string line ) = > string . IsNullOrWhiteSpace ( line ) | | line . AsSpan ( ) . TrimStart ( ) . StartsWith ( "//" . AsSpan ( ) , StringComparison . Ordinal ) ;
2018-04-13 18:19:50 +09:00
2020-03-30 17:18:09 +09:00
/// <summary>
/// Invoked when a new <see cref="Section"/> has been entered.
/// </summary>
/// <param name="section">The entered <see cref="Section"/>.</param>
protected virtual void OnBeginNewSection ( Section section )
{
}
2018-03-13 19:13:50 +09:00
protected virtual void ParseLine ( T output , Section section , string line )
{
switch ( section )
{
case Section . Colours :
2022-09-21 16:04:32 +09:00
HandleColours ( output , line , false ) ;
2018-03-13 19:13:50 +09:00
return ;
}
}
2018-07-16 16:35:55 +02:00
protected string StripComments ( string line )
2018-07-16 01:04:41 +02:00
{
2021-10-27 13:04:41 +09:00
int index = line . AsSpan ( ) . IndexOf ( "//" . AsSpan ( ) ) ;
2018-07-16 01:04:41 +02:00
if ( index > 0 )
return line . Substring ( 0 , index ) ;
2019-02-28 13:31:40 +09:00
2018-07-16 01:04:41 +02:00
return line ;
}
2018-04-13 18:19:50 +09:00
2022-09-21 16:04:32 +09:00
protected void HandleColours < TModel > ( TModel output , string line , bool allowAlpha )
2018-03-13 19:13:50 +09:00
{
2018-03-14 18:41:48 +09:00
var pair = SplitKeyVal ( line ) ;
2018-04-13 18:19:50 +09:00
2020-10-16 12:49:31 +09:00
bool isCombo = pair . Key . StartsWith ( @"Combo" , StringComparison . Ordinal ) ;
2018-04-13 18:19:50 +09:00
2018-07-16 01:04:41 +02:00
string [ ] split = pair . Value . Split ( ',' ) ;
2018-04-13 18:19:50 +09:00
2018-10-05 11:19:01 +09:00
if ( split . Length ! = 3 & & split . Length ! = 4 )
throw new InvalidOperationException ( $@"Color specified in incorrect format (should be R,G,B or R,G,B,A): {pair.Value}" ) ;
2018-04-13 18:19:50 +09:00
2018-10-05 11:55:59 +09:00
Color4 colour ;
2018-10-05 11:19:01 +09:00
2018-10-05 11:55:59 +09:00
try
{
2022-09-21 16:04:32 +09:00
byte alpha = allowAlpha & & split . Length = = 4 ? byte . Parse ( split [ 3 ] ) : ( byte ) 255 ;
2020-06-25 14:15:26 +09:00
colour = new Color4 ( byte . Parse ( split [ 0 ] ) , byte . Parse ( split [ 1 ] ) , byte . Parse ( split [ 2 ] ) , alpha ) ;
2018-10-05 11:55:59 +09:00
}
2019-04-25 17:36:17 +09:00
catch
2018-10-05 11:19:01 +09:00
{
2018-03-13 19:13:50 +09:00
throw new InvalidOperationException ( @"Color must be specified with 8-bit integer components" ) ;
2018-10-05 11:19:01 +09:00
}
2018-04-13 18:19:50 +09:00
2018-03-13 19:13:50 +09:00
if ( isCombo )
{
if ( ! ( output is IHasComboColours tHasComboColours ) ) return ;
2018-04-13 18:19:50 +09:00
2021-08-15 16:00:22 +02:00
tHasComboColours . CustomComboColours . Add ( colour ) ;
2018-03-13 19:13:50 +09:00
}
else
{
if ( ! ( output is IHasCustomColours tHasCustomColours ) ) return ;
2019-02-28 13:31:40 +09:00
2018-03-13 19:13:50 +09:00
tHasCustomColours . CustomColours [ pair . Key ] = colour ;
}
}
2018-04-13 18:19:50 +09:00
2018-03-14 18:41:48 +09:00
protected KeyValuePair < string , string > SplitKeyVal ( string line , char separator = ':' )
2017-12-01 17:43:33 +01:00
{
2021-10-27 13:04:41 +09:00
string [ ] split = line . Split ( separator , 2 ) ;
2018-04-13 18:19:50 +09:00
2018-01-04 19:04:52 +08:00
return new KeyValuePair < string , string >
(
split [ 0 ] . Trim ( ) ,
split . Length > 1 ? split [ 1 ] . Trim ( ) : string . Empty
) ;
2017-12-01 17:43:33 +01:00
}
2018-04-13 18:19:50 +09:00
2020-01-23 16:23:53 +01:00
protected string CleanFilename ( string path ) = > path . Trim ( '"' ) . ToStandardisedPath ( ) ;
2022-06-13 15:40:11 +09:00
public enum Section
2017-12-01 17:43:33 +01:00
{
General ,
Editor ,
Metadata ,
Difficulty ,
Events ,
TimingPoints ,
Colours ,
HitObjects ,
Variables ,
2020-03-30 17:18:09 +09:00
Fonts ,
2020-04-05 00:10:12 +03:00
CatchTheBeat ,
Mania ,
2017-12-01 17:43:33 +01:00
}
2018-04-13 18:19:50 +09:00
2020-07-13 17:06:00 +09:00
[Obsolete("Do not use unless you're a legacy ruleset and 100% sure.")]
2022-06-20 14:56:04 +09:00
public class LegacyDifficultyControlPoint : DifficultyControlPoint , IEquatable < LegacyDifficultyControlPoint >
2019-10-28 19:10:39 +09:00
{
2020-07-13 17:06:00 +09:00
/// <summary>
/// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it.
/// DO NOT USE THIS UNLESS 100% SURE.
/// </summary>
2021-01-12 17:50:22 +09:00
public double BpmMultiplier { get ; private set ; }
2020-07-13 17:06:00 +09:00
2022-08-23 14:07:18 -04:00
/// <summary>
/// Whether or not slider ticks should be generated at this control point.
/// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991).
/// </summary>
public bool GenerateTicks { get ; private set ; } = true ;
2022-10-13 15:05:15 +09:00
public LegacyDifficultyControlPoint ( int rulesetId , double beatLength )
2021-01-04 16:37:07 +09:00
: this ( )
{
2021-01-12 17:50:22 +09:00
// Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?).
2022-10-13 15:05:15 +09:00
if ( rulesetId = = 1 | | rulesetId = = 3 )
BpmMultiplier = beatLength < 0 ? Math . Clamp ( ( float ) - beatLength , 10 , 10000 ) / 100.0 : 1 ;
else
BpmMultiplier = beatLength < 0 ? Math . Clamp ( ( float ) - beatLength , 10 , 1000 ) / 100.0 : 1 ;
2022-08-23 14:07:18 -04:00
GenerateTicks = ! double . IsNaN ( beatLength ) ;
2021-01-04 16:37:07 +09:00
}
public LegacyDifficultyControlPoint ( )
2019-10-28 19:10:39 +09:00
{
2021-08-31 23:59:36 +09:00
SliderVelocityBindable . Precision = double . Epsilon ;
2021-01-04 16:37:07 +09:00
}
2020-07-13 17:06:00 +09:00
2022-08-23 14:07:18 -04:00
public override bool IsRedundant ( ControlPoint ? existing )
2022-08-24 03:37:33 -04:00
= > base . IsRedundant ( existing )
& & GenerateTicks = = ( ( existing as LegacyDifficultyControlPoint ) ? . GenerateTicks ? ? true ) ;
2022-08-23 14:07:18 -04:00
2021-01-04 16:37:07 +09:00
public override void CopyFrom ( ControlPoint other )
{
base . CopyFrom ( other ) ;
BpmMultiplier = ( ( LegacyDifficultyControlPoint ) other ) . BpmMultiplier ;
2022-08-23 14:07:18 -04:00
GenerateTicks = ( ( LegacyDifficultyControlPoint ) other ) . GenerateTicks ;
2019-10-28 19:10:39 +09:00
}
2022-06-20 14:56:04 +09:00
public override bool Equals ( ControlPoint ? other )
= > other is LegacyDifficultyControlPoint otherLegacyDifficultyControlPoint
& & Equals ( otherLegacyDifficultyControlPoint ) ;
public bool Equals ( LegacyDifficultyControlPoint ? other )
= > base . Equals ( other )
2022-08-23 14:07:18 -04:00
& & BpmMultiplier = = other . BpmMultiplier
& & GenerateTicks = = other . GenerateTicks ;
2022-06-20 14:56:04 +09:00
2022-08-23 14:07:18 -04:00
// ReSharper disable twice NonReadonlyMemberInGetHashCode
2022-10-13 15:05:15 +09:00
public override int GetHashCode ( ) = > HashCode . Combine ( base . GetHashCode ( ) , BpmMultiplier , GenerateTicks ) ;
2019-10-28 19:10:39 +09:00
}
2022-06-20 14:56:04 +09:00
internal class LegacySampleControlPoint : SampleControlPoint , IEquatable < LegacySampleControlPoint >
2018-06-28 18:20:43 +09:00
{
public int CustomSampleBank ;
2019-06-30 21:58:30 +09:00
public override HitSampleInfo ApplyTo ( HitSampleInfo hitSampleInfo )
2018-06-28 18:20:43 +09:00
{
2019-06-30 21:58:30 +09:00
var baseInfo = base . ApplyTo ( hitSampleInfo ) ;
2018-06-28 18:20:43 +09:00
2020-12-01 15:37:51 +09:00
if ( baseInfo is ConvertHitObjectParser . LegacyHitSampleInfo legacy & & legacy . CustomSampleBank = = 0 )
2020-12-02 10:55:48 +09:00
return legacy . With ( newCustomSampleBank : CustomSampleBank ) ;
2018-06-28 18:20:43 +09:00
return baseInfo ;
}
2022-06-20 16:52:01 +09:00
public override bool IsRedundant ( ControlPoint ? existing )
2020-04-17 17:06:12 +09:00
= > base . IsRedundant ( existing )
2020-04-17 17:04:09 +09:00
& & existing is LegacySampleControlPoint existingSample
& & CustomSampleBank = = existingSample . CustomSampleBank ;
2021-01-05 13:41:31 +09:00
public override void CopyFrom ( ControlPoint other )
{
base . CopyFrom ( other ) ;
CustomSampleBank = ( ( LegacySampleControlPoint ) other ) . CustomSampleBank ;
}
2022-06-20 14:56:04 +09:00
public override bool Equals ( ControlPoint ? other )
= > other is LegacySampleControlPoint otherLegacySampleControlPoint
& & Equals ( otherLegacySampleControlPoint ) ;
public bool Equals ( LegacySampleControlPoint ? other )
= > base . Equals ( other )
& & CustomSampleBank = = other . CustomSampleBank ;
// ReSharper disable once NonReadonlyMemberInGetHashCode
public override int GetHashCode ( ) = > HashCode . Combine ( base . GetHashCode ( ) , CustomSampleBank ) ;
2018-06-28 18:20:43 +09:00
}
2017-12-01 17:43:33 +01:00
}
}