2019-01-24 16:43:03 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
2017-12-02 00:43:33 +08:00
using System ;
using System.Collections.Generic ;
2020-01-23 23:23:53 +08:00
using osu.Framework.Extensions ;
2018-03-22 11:58:02 +08:00
using osu.Framework.Logging ;
2018-06-28 17:20:43 +08:00
using osu.Game.Audio ;
using osu.Game.Beatmaps.ControlPoints ;
2019-09-10 06:43:30 +08:00
using osu.Game.IO ;
2020-04-14 20:05:07 +08:00
using osu.Game.Rulesets.Objects.Legacy ;
2018-11-20 15:51:59 +08:00
using osuTK.Graphics ;
2018-04-13 17:19:50 +08:00
2017-12-02 00:43:33 +08:00
namespace osu.Game.Beatmaps.Formats
{
2018-03-09 20:23:03 +08:00
public abstract class LegacyDecoder < T > : Decoder < T >
where T : new ( )
2017-12-02 00:43:33 +08:00
{
2020-11-12 16:03:42 +08:00
public const int LATEST_VERSION = 14 ;
2018-03-09 20:23:03 +08:00
protected readonly int FormatVersion ;
2018-04-13 17:19:50 +08:00
2018-03-09 20:23:03 +08:00
protected LegacyDecoder ( int version )
2017-12-02 00:43:33 +08:00
{
2018-03-09 20:23:03 +08:00
FormatVersion = version ;
2017-12-02 00:43:33 +08:00
}
2018-04-13 17:19:50 +08:00
2019-09-10 06:43:30 +08:00
protected override void ParseStreamInto ( LineBufferedReader stream , T output )
2017-12-02 00:43:33 +08:00
{
2021-11-02 12:17:21 +08:00
Section section = Section . General ;
2018-04-13 17:19:50 +08:00
2022-07-06 13:29:55 +08:00
string? line ;
2019-04-01 11:16:05 +08:00
2017-12-02 00:43:33 +08:00
while ( ( line = stream . ReadLine ( ) ) ! = null )
{
2017-12-02 23:05:39 +08:00
if ( ShouldSkipLine ( line ) )
2017-12-02 00:43:33 +08:00
continue ;
2018-04-13 17:19:50 +08:00
2021-03-25 12:35:54 +08: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 14:36:07 +08:00
2020-02-09 01:05:27 +08:00
if ( line . StartsWith ( '[' ) & & line . EndsWith ( ']' ) )
2017-12-02 00:43:33 +08:00
{
2019-12-14 20:54:22 +08:00
if ( ! Enum . TryParse ( line [ 1. . ^ 1 ] , out section ) )
2019-08-12 00:42:05 +08:00
Logger . Log ( $"Unknown section \" { line } \ " in \"{output}\"" ) ;
2018-04-13 17:19:50 +08:00
2020-03-30 16:18:09 +08:00
OnBeginNewSection ( section ) ;
2017-12-02 00:43:33 +08:00
continue ;
}
2018-04-13 17:19:50 +08:00
2019-08-07 18:33:54 +08:00
try
{
ParseLine ( output , section , line ) ;
}
catch ( Exception e )
{
2022-05-27 18:18:37 +08:00
Logger . Log ( $"Failed to process line \" { line } \ " into \"{output}\": {e.Message}" ) ;
2019-08-07 18:33:54 +08:00
}
2017-12-02 00:43:33 +08:00
}
}
2018-04-13 17:19:50 +08:00
2019-05-14 15:16:55 +08:00
protected virtual bool ShouldSkipLine ( string line ) = > string . IsNullOrWhiteSpace ( line ) | | line . AsSpan ( ) . TrimStart ( ) . StartsWith ( "//" . AsSpan ( ) , StringComparison . Ordinal ) ;
2018-04-13 17:19:50 +08:00
2020-03-30 16:18:09 +08: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 18:13:50 +08:00
protected virtual void ParseLine ( T output , Section section , string line )
{
switch ( section )
{
case Section . Colours :
2022-09-21 15:04:32 +08:00
HandleColours ( output , line , false ) ;
2018-03-13 18:13:50 +08:00
return ;
}
}
2018-07-16 22:35:55 +08:00
protected string StripComments ( string line )
2018-07-16 07:04:41 +08:00
{
2021-10-27 12:04:41 +08:00
int index = line . AsSpan ( ) . IndexOf ( "//" . AsSpan ( ) ) ;
2018-07-16 07:04:41 +08:00
if ( index > 0 )
return line . Substring ( 0 , index ) ;
2019-02-28 12:31:40 +08:00
2018-07-16 07:04:41 +08:00
return line ;
}
2018-04-13 17:19:50 +08:00
2022-09-21 15:04:32 +08:00
protected void HandleColours < TModel > ( TModel output , string line , bool allowAlpha )
2018-03-13 18:13:50 +08:00
{
2018-03-14 17:41:48 +08:00
var pair = SplitKeyVal ( line ) ;
2018-04-13 17:19:50 +08:00
2020-10-16 11:49:31 +08:00
bool isCombo = pair . Key . StartsWith ( @"Combo" , StringComparison . Ordinal ) ;
2018-04-13 17:19:50 +08:00
2018-07-16 07:04:41 +08:00
string [ ] split = pair . Value . Split ( ',' ) ;
2018-04-13 17:19:50 +08:00
2018-10-05 10:19:01 +08: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 17:19:50 +08:00
2018-10-05 10:55:59 +08:00
Color4 colour ;
2018-10-05 10:19:01 +08:00
2018-10-05 10:55:59 +08:00
try
{
2022-09-21 15:04:32 +08:00
byte alpha = allowAlpha & & split . Length = = 4 ? byte . Parse ( split [ 3 ] ) : ( byte ) 255 ;
2020-06-25 13:15:26 +08:00
colour = new Color4 ( byte . Parse ( split [ 0 ] ) , byte . Parse ( split [ 1 ] ) , byte . Parse ( split [ 2 ] ) , alpha ) ;
2018-10-05 10:55:59 +08:00
}
2019-04-25 16:36:17 +08:00
catch
2018-10-05 10:19:01 +08:00
{
2018-03-13 18:13:50 +08:00
throw new InvalidOperationException ( @"Color must be specified with 8-bit integer components" ) ;
2018-10-05 10:19:01 +08:00
}
2018-04-13 17:19:50 +08:00
2018-03-13 18:13:50 +08:00
if ( isCombo )
{
if ( ! ( output is IHasComboColours tHasComboColours ) ) return ;
2018-04-13 17:19:50 +08:00
2021-08-15 22:00:22 +08:00
tHasComboColours . CustomComboColours . Add ( colour ) ;
2018-03-13 18:13:50 +08:00
}
else
{
if ( ! ( output is IHasCustomColours tHasCustomColours ) ) return ;
2019-02-28 12:31:40 +08:00
2018-03-13 18:13:50 +08:00
tHasCustomColours . CustomColours [ pair . Key ] = colour ;
}
}
2018-04-13 17:19:50 +08:00
2022-11-08 12:30:11 +08:00
protected KeyValuePair < string , string > SplitKeyVal ( string line , char separator = ':' , bool shouldTrim = true )
2017-12-02 00:43:33 +08:00
{
2022-12-27 16:41:58 +08:00
string [ ] split = line . Split ( separator , 2 , shouldTrim ? StringSplitOptions . TrimEntries : StringSplitOptions . None ) ;
2022-11-08 12:30:11 +08:00
2018-01-04 19:04:52 +08:00
return new KeyValuePair < string , string >
(
2022-11-08 12:30:11 +08:00
split [ 0 ] ,
split . Length > 1 ? split [ 1 ] : string . Empty
2018-01-04 19:04:52 +08:00
) ;
2017-12-02 00:43:33 +08:00
}
2018-04-13 17:19:50 +08:00
2022-11-21 15:06:36 +08:00
protected string CleanFilename ( string path ) = > path
// User error which is supported by stable (https://github.com/ppy/osu/issues/21204)
. Replace ( @"\\" , @"\" )
. Trim ( '"' )
. ToStandardisedPath ( ) ;
2020-01-23 23:23:53 +08:00
2022-06-13 14:40:11 +08:00
public enum Section
2017-12-02 00:43:33 +08:00
{
General ,
Editor ,
Metadata ,
Difficulty ,
Events ,
TimingPoints ,
Colours ,
HitObjects ,
Variables ,
2020-03-30 16:18:09 +08:00
Fonts ,
2020-04-05 05:10:12 +08:00
CatchTheBeat ,
Mania ,
2017-12-02 00:43:33 +08:00
}
2018-04-13 17:19:50 +08:00
2020-07-13 16:06:00 +08:00
[Obsolete("Do not use unless you're a legacy ruleset and 100% sure.")]
2022-06-20 13:56:04 +08:00
public class LegacyDifficultyControlPoint : DifficultyControlPoint , IEquatable < LegacyDifficultyControlPoint >
2019-10-28 18:10:39 +08:00
{
2020-07-13 16:06:00 +08: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 16:50:22 +08:00
public double BpmMultiplier { get ; private set ; }
2020-07-13 16:06:00 +08:00
2022-08-24 02:07:18 +08: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 14:05:15 +08:00
public LegacyDifficultyControlPoint ( int rulesetId , double beatLength )
2021-01-04 15:37:07 +08:00
: this ( )
{
2021-01-12 16:50:22 +08: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 14:05:15 +08: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-24 02:07:18 +08:00
GenerateTicks = ! double . IsNaN ( beatLength ) ;
2021-01-04 15:37:07 +08:00
}
public LegacyDifficultyControlPoint ( )
2019-10-28 18:10:39 +08:00
{
2021-08-31 22:59:36 +08:00
SliderVelocityBindable . Precision = double . Epsilon ;
2021-01-04 15:37:07 +08:00
}
2020-07-13 16:06:00 +08:00
2022-08-24 02:07:18 +08:00
public override bool IsRedundant ( ControlPoint ? existing )
2022-08-24 15:37:33 +08:00
= > base . IsRedundant ( existing )
& & GenerateTicks = = ( ( existing as LegacyDifficultyControlPoint ) ? . GenerateTicks ? ? true ) ;
2022-08-24 02:07:18 +08:00
2021-01-04 15:37:07 +08:00
public override void CopyFrom ( ControlPoint other )
{
base . CopyFrom ( other ) ;
BpmMultiplier = ( ( LegacyDifficultyControlPoint ) other ) . BpmMultiplier ;
2022-08-24 02:07:18 +08:00
GenerateTicks = ( ( LegacyDifficultyControlPoint ) other ) . GenerateTicks ;
2019-10-28 18:10:39 +08:00
}
2022-06-20 13:56:04 +08:00
public override bool Equals ( ControlPoint ? other )
= > other is LegacyDifficultyControlPoint otherLegacyDifficultyControlPoint
& & Equals ( otherLegacyDifficultyControlPoint ) ;
public bool Equals ( LegacyDifficultyControlPoint ? other )
= > base . Equals ( other )
2022-08-24 02:07:18 +08:00
& & BpmMultiplier = = other . BpmMultiplier
& & GenerateTicks = = other . GenerateTicks ;
2022-06-20 13:56:04 +08:00
2022-08-24 02:07:18 +08:00
// ReSharper disable twice NonReadonlyMemberInGetHashCode
2022-10-13 14:05:15 +08:00
public override int GetHashCode ( ) = > HashCode . Combine ( base . GetHashCode ( ) , BpmMultiplier , GenerateTicks ) ;
2019-10-28 18:10:39 +08:00
}
2022-06-20 13:56:04 +08:00
internal class LegacySampleControlPoint : SampleControlPoint , IEquatable < LegacySampleControlPoint >
2018-06-28 17:20:43 +08:00
{
public int CustomSampleBank ;
2019-06-30 20:58:30 +08:00
public override HitSampleInfo ApplyTo ( HitSampleInfo hitSampleInfo )
2018-06-28 17:20:43 +08:00
{
2023-05-16 15:29:24 +08:00
if ( hitSampleInfo is ConvertHitObjectParser . LegacyHitSampleInfo legacy )
{
return legacy . With (
newCustomSampleBank : legacy . CustomSampleBank > 0 ? legacy . CustomSampleBank : CustomSampleBank ,
newVolume : hitSampleInfo . Volume > 0 ? hitSampleInfo . Volume : SampleVolume ,
newBank : legacy . BankSpecified ? legacy . Bank : SampleBank
) ;
}
2018-06-28 17:20:43 +08:00
2023-05-16 15:29:24 +08:00
return base . ApplyTo ( hitSampleInfo ) ;
2018-06-28 17:20:43 +08:00
}
2022-06-20 15:52:01 +08:00
public override bool IsRedundant ( ControlPoint ? existing )
2020-04-17 16:06:12 +08:00
= > base . IsRedundant ( existing )
2020-04-17 16:04:09 +08:00
& & existing is LegacySampleControlPoint existingSample
& & CustomSampleBank = = existingSample . CustomSampleBank ;
2021-01-05 12:41:31 +08:00
public override void CopyFrom ( ControlPoint other )
{
base . CopyFrom ( other ) ;
CustomSampleBank = ( ( LegacySampleControlPoint ) other ) . CustomSampleBank ;
}
2022-06-20 13:56:04 +08: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 17:20:43 +08:00
}
2017-12-02 00:43:33 +08:00
}
}