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
2022-06-17 15:38:35 +08:00
#nullable disable
2018-11-20 15:51:59 +08:00
using osuTK ;
2017-04-18 15:05:58 +08:00
using osu.Game.Rulesets.Objects.Types ;
2016-12-06 17:56:20 +08:00
using System ;
2016-11-14 21:03:39 +08:00
using System.Collections.Generic ;
2018-07-02 13:20:35 +08:00
using System.IO ;
2017-04-06 08:43:47 +08:00
using osu.Game.Beatmaps.Formats ;
2017-04-06 10:41:16 +08:00
using osu.Game.Audio ;
2017-05-12 15:35:57 +08:00
using System.Linq ;
2018-08-22 14:35:29 +08:00
using JetBrains.Annotations ;
2021-02-25 14:38:56 +08:00
using osu.Framework.Extensions.EnumExtensions ;
2020-01-09 12:43:44 +08:00
using osu.Framework.Utils ;
2023-05-16 15:29:24 +08:00
using osu.Game.Beatmaps.ControlPoints ;
2019-12-10 19:19:16 +08:00
using osu.Game.Beatmaps.Legacy ;
2020-06-21 22:43:21 +08:00
using osu.Game.Skinning ;
2020-12-01 14:37:51 +08:00
using osu.Game.Utils ;
2024-02-28 22:51:36 +08:00
using System.Buffers ;
2018-04-13 17:19:50 +08:00
2017-04-18 15:05:58 +08:00
namespace osu.Game.Rulesets.Objects.Legacy
2016-11-14 21:03:39 +08:00
{
2017-04-18 08:13:36 +08:00
/// <summary>
/// A HitObjectParser to parse legacy Beatmaps.
/// </summary>
2017-11-21 11:11:29 +08:00
public abstract class ConvertHitObjectParser : HitObjectParser
2016-11-14 21:03:39 +08:00
{
2018-08-15 09:24:56 +08:00
/// <summary>
/// The offset to apply to all time values.
/// </summary>
2018-08-15 10:47:31 +08:00
protected readonly double Offset ;
2018-08-15 09:24:56 +08:00
/// <summary>
/// The beatmap version.
/// </summary>
2018-08-15 10:47:31 +08:00
protected readonly int FormatVersion ;
2018-08-15 09:24:56 +08:00
2018-08-15 10:47:54 +08:00
protected bool FirstObject { get ; private set ; } = true ;
2018-08-15 09:24:56 +08:00
protected ConvertHitObjectParser ( double offset , int formatVersion )
2018-04-30 15:43:32 +08:00
{
2018-08-15 09:24:56 +08:00
Offset = offset ;
2018-08-15 09:53:25 +08:00
FormatVersion = formatVersion ;
2018-04-30 15:43:32 +08:00
}
2018-08-15 09:24:56 +08:00
public override HitObject Parse ( string text )
2016-11-14 21:03:39 +08:00
{
2019-08-08 13:44:04 +08:00
string [ ] split = text . Split ( ',' ) ;
2018-04-13 17:19:50 +08:00
2019-08-08 13:44:04 +08:00
Vector2 pos = new Vector2 ( ( int ) Parsing . ParseFloat ( split [ 0 ] , Parsing . MAX_COORDINATE_VALUE ) , ( int ) Parsing . ParseFloat ( split [ 1 ] , Parsing . MAX_COORDINATE_VALUE ) ) ;
2018-07-19 23:47:55 +08:00
2019-08-08 13:44:04 +08:00
double startTime = Parsing . ParseDouble ( split [ 2 ] ) + Offset ;
2019-03-12 19:31:15 +08:00
2019-12-10 19:19:16 +08:00
LegacyHitObjectType type = ( LegacyHitObjectType ) Parsing . ParseInt ( split [ 3 ] ) ;
2018-08-15 09:48:42 +08:00
2019-12-10 19:19:16 +08:00
int comboOffset = ( int ) ( type & LegacyHitObjectType . ComboOffset ) > > 4 ;
type & = ~ LegacyHitObjectType . ComboOffset ;
2018-08-15 09:48:42 +08:00
2021-02-25 14:38:56 +08:00
bool combo = type . HasFlagFast ( LegacyHitObjectType . NewCombo ) ;
2019-12-10 19:19:16 +08:00
type & = ~ LegacyHitObjectType . NewCombo ;
2018-04-13 17:19:50 +08:00
2019-12-10 19:04:37 +08:00
var soundType = ( LegacyHitSoundType ) Parsing . ParseInt ( split [ 4 ] ) ;
2019-08-08 13:44:04 +08:00
var bankInfo = new SampleBankInfo ( ) ;
2018-04-13 17:19:50 +08:00
2019-08-08 13:44:04 +08:00
HitObject result = null ;
2018-04-13 17:19:50 +08:00
2021-02-25 14:38:56 +08:00
if ( type . HasFlagFast ( LegacyHitObjectType . Circle ) )
2019-08-08 13:44:04 +08:00
{
result = CreateHit ( pos , combo , comboOffset ) ;
2018-04-13 17:19:50 +08:00
2019-08-08 13:44:04 +08:00
if ( split . Length > 5 )
readCustomSampleBanks ( split [ 5 ] , bankInfo ) ;
}
2021-02-25 14:38:56 +08:00
else if ( type . HasFlagFast ( LegacyHitObjectType . Slider ) )
2019-08-08 13:44:04 +08:00
{
double? length = null ;
2018-04-13 17:19:50 +08:00
2019-08-08 13:44:04 +08:00
int repeatCount = Parsing . ParseInt ( split [ 6 ] ) ;
2018-04-13 17:19:50 +08:00
2019-08-08 13:44:04 +08:00
if ( repeatCount > 9000 )
2019-11-28 22:21:21 +08:00
throw new FormatException ( @"Repeat count is way too high" ) ;
2018-04-13 17:19:50 +08:00
2019-08-08 13:44:04 +08:00
// osu-stable treated the first span of the slider as a repeat, but no repeats are happening
repeatCount = Math . Max ( 0 , repeatCount - 1 ) ;
2018-04-13 17:19:50 +08:00
2019-08-08 13:44:04 +08:00
if ( split . Length > 7 )
{
2020-03-11 17:07:11 +08:00
length = Math . Max ( 0 , Parsing . ParseDouble ( split [ 7 ] , Parsing . MAX_COORDINATE_VALUE ) ) ;
2019-08-08 13:44:04 +08:00
if ( length = = 0 )
length = null ;
}
2018-04-13 17:19:50 +08:00
2019-08-08 13:44:04 +08:00
if ( split . Length > 10 )
2023-04-30 05:51:49 +08:00
readCustomSampleBanks ( split [ 10 ] , bankInfo , true ) ;
2018-04-13 17:19:50 +08:00
2019-08-08 13:44:04 +08:00
// One node for each repeat + the start and end nodes
int nodes = repeatCount + 2 ;
2019-04-01 11:16:05 +08:00
2019-08-08 13:44:04 +08:00
// Populate node sample bank infos with the default hit object sample bank
var nodeBankInfos = new List < SampleBankInfo > ( ) ;
for ( int i = 0 ; i < nodes ; i + + )
nodeBankInfos . Add ( bankInfo . Clone ( ) ) ;
2018-04-13 17:19:50 +08:00
2019-08-08 13:44:04 +08:00
// Read any per-node sample banks
if ( split . Length > 9 & & split [ 9 ] . Length > 0 )
{
string [ ] sets = split [ 9 ] . Split ( '|' ) ;
2018-04-13 17:19:50 +08:00
2017-04-21 15:18:00 +08:00
for ( int i = 0 ; i < nodes ; i + + )
2017-04-21 13:38:46 +08:00
{
2019-08-08 13:44:04 +08:00
if ( i > = sets . Length )
break ;
2019-04-01 11:16:05 +08:00
2019-08-08 13:44:04 +08:00
SampleBankInfo info = nodeBankInfos [ i ] ;
readCustomSampleBanks ( sets [ i ] , info ) ;
2017-04-21 13:38:46 +08:00
}
2017-08-10 14:50:20 +08:00
}
2019-03-12 19:31:15 +08:00
2019-08-08 13:44:04 +08:00
// Populate node sound types with the default hit object sound type
2019-12-10 19:04:37 +08:00
var nodeSoundTypes = new List < LegacyHitSoundType > ( ) ;
2019-08-08 13:44:04 +08:00
for ( int i = 0 ; i < nodes ; i + + )
nodeSoundTypes . Add ( soundType ) ;
2018-04-13 17:19:50 +08:00
2019-08-08 13:44:04 +08:00
// Read any per-node sound types
if ( split . Length > 8 & & split [ 8 ] . Length > 0 )
2017-08-10 14:50:20 +08:00
{
2019-08-08 13:44:04 +08:00
string [ ] adds = split [ 8 ] . Split ( '|' ) ;
2018-04-13 17:19:50 +08:00
2019-08-08 13:44:04 +08:00
for ( int i = 0 ; i < nodes ; i + + )
2017-08-10 14:50:20 +08:00
{
2019-08-08 13:44:04 +08:00
if ( i > = adds . Length )
break ;
2018-04-13 17:19:50 +08:00
2021-10-27 12:04:41 +08:00
int . TryParse ( adds [ i ] , out int sound ) ;
2019-12-10 19:04:37 +08:00
nodeSoundTypes [ i ] = ( LegacyHitSoundType ) sound ;
2019-08-08 13:44:04 +08:00
}
2018-08-22 14:35:29 +08:00
}
2018-04-13 17:19:50 +08:00
2019-08-08 13:44:04 +08:00
// Generate the final per-node samples
2019-11-08 13:04:57 +08:00
var nodeSamples = new List < IList < HitSampleInfo > > ( nodes ) ;
2019-08-08 13:44:04 +08:00
for ( int i = 0 ; i < nodes ; i + + )
nodeSamples . Add ( convertSoundType ( nodeSoundTypes [ i ] , nodeBankInfos [ i ] ) ) ;
2018-10-16 16:10:24 +08:00
2020-10-12 18:16:37 +08:00
result = CreateSlider ( pos , combo , comboOffset , convertPathString ( split [ 5 ] , pos ) , length , repeatCount , nodeSamples ) ;
2017-08-10 14:50:20 +08:00
}
2021-02-25 14:38:56 +08:00
else if ( type . HasFlagFast ( LegacyHitObjectType . Spinner ) )
2017-08-10 14:50:20 +08:00
{
2020-05-27 11:37:44 +08:00
double duration = Math . Max ( 0 , Parsing . ParseDouble ( split [ 5 ] ) + Offset - startTime ) ;
2019-08-08 13:44:04 +08:00
2020-05-27 11:37:44 +08:00
result = CreateSpinner ( new Vector2 ( 512 , 384 ) / 2 , combo , comboOffset , duration ) ;
2019-08-08 13:44:04 +08:00
if ( split . Length > 6 )
readCustomSampleBanks ( split [ 6 ] , bankInfo ) ;
2017-08-10 14:50:20 +08:00
}
2021-02-25 14:38:56 +08:00
else if ( type . HasFlagFast ( LegacyHitObjectType . Hold ) )
2019-03-13 10:30:33 +08:00
{
2019-08-08 13:44:04 +08:00
// Note: Hold is generated by BMS converts
double endTime = Math . Max ( startTime , Parsing . ParseDouble ( split [ 2 ] ) ) ;
if ( split . Length > 5 & & ! string . IsNullOrEmpty ( split [ 5 ] ) )
{
string [ ] ss = split [ 5 ] . Split ( ':' ) ;
endTime = Math . Max ( startTime , Parsing . ParseDouble ( ss [ 0 ] ) ) ;
2020-10-16 17:52:29 +08:00
readCustomSampleBanks ( string . Join ( ':' , ss . Skip ( 1 ) ) , bankInfo ) ;
2019-08-08 13:44:04 +08:00
}
2020-05-27 11:37:44 +08:00
result = CreateHold ( pos , combo , comboOffset , endTime + Offset - startTime ) ;
2019-03-13 10:30:33 +08:00
}
2019-08-08 13:44:04 +08:00
if ( result = = null )
2019-08-08 13:44:49 +08:00
throw new InvalidDataException ( $"Unknown hit object type: {split[3]}" ) ;
2019-08-08 13:44:04 +08:00
result . StartTime = startTime ;
if ( result . Samples . Count = = 0 )
result . Samples = convertSoundType ( soundType , bankInfo ) ;
FirstObject = false ;
return result ;
2016-11-14 21:03:39 +08:00
}
2018-04-13 17:19:50 +08:00
2023-04-30 05:51:49 +08:00
private void readCustomSampleBanks ( string str , SampleBankInfo bankInfo , bool banksOnly = false )
2017-04-06 08:43:47 +08:00
{
if ( string . IsNullOrEmpty ( str ) )
return ;
2018-04-13 17:19:50 +08:00
2017-04-06 08:43:47 +08:00
string [ ] split = str . Split ( ':' ) ;
2018-04-13 17:19:50 +08:00
2019-12-10 19:23:15 +08:00
var bank = ( LegacySampleBank ) Parsing . ParseInt ( split [ 0 ] ) ;
2023-09-19 19:53:49 +08:00
if ( ! Enum . IsDefined ( bank ) )
bank = LegacySampleBank . Normal ;
2021-12-10 13:15:00 +08:00
var addBank = ( LegacySampleBank ) Parsing . ParseInt ( split [ 1 ] ) ;
2023-09-19 19:53:49 +08:00
if ( ! Enum . IsDefined ( addBank ) )
addBank = LegacySampleBank . Normal ;
2018-04-13 17:19:50 +08:00
2018-07-25 13:37:05 +08:00
string stringBank = bank . ToString ( ) . ToLowerInvariant ( ) ;
2023-09-19 19:53:49 +08:00
if ( stringBank = = @"none" )
2017-04-06 11:27:35 +08:00
stringBank = null ;
2021-12-10 13:15:00 +08:00
string stringAddBank = addBank . ToString ( ) . ToLowerInvariant ( ) ;
2023-09-19 19:53:49 +08:00
if ( stringAddBank = = @"none" )
2017-04-06 11:27:35 +08:00
stringAddBank = null ;
2018-04-13 17:19:50 +08:00
2022-10-25 13:55:33 +08:00
bankInfo . BankForNormal = stringBank ;
bankInfo . BankForAdditions = string . IsNullOrEmpty ( stringAddBank ) ? stringBank : stringAddBank ;
2018-04-13 17:19:50 +08:00
2023-04-30 05:51:49 +08:00
if ( banksOnly ) return ;
2018-07-20 14:12:44 +08:00
if ( split . Length > 2 )
2019-03-12 19:31:15 +08:00
bankInfo . CustomSampleBank = Parsing . ParseInt ( split [ 2 ] ) ;
2018-07-20 14:12:44 +08:00
2017-04-21 13:36:28 +08:00
if ( split . Length > 3 )
2019-03-13 11:35:05 +08:00
bankInfo . Volume = Math . Max ( 0 , Parsing . ParseInt ( split [ 3 ] ) ) ;
2018-07-02 13:29:18 +08:00
bankInfo . Filename = split . Length > 4 ? split [ 4 ] : null ;
2017-04-06 08:43:47 +08:00
}
2018-04-13 17:19:50 +08:00
2020-10-12 17:04:28 +08:00
private PathType convertPathType ( string input )
{
switch ( input [ 0 ] )
{
default :
case 'C' :
2023-11-13 15:24:09 +08:00
return PathType . CATMULL ;
2020-10-12 17:04:28 +08:00
case 'B' :
2023-11-08 18:43:54 +08:00
if ( input . Length > 1 & & int . TryParse ( input . Substring ( 1 ) , out int degree ) & & degree > 0 )
2023-11-13 15:24:09 +08:00
return PathType . BSpline ( degree ) ;
2023-11-11 22:02:06 +08:00
2023-11-20 12:35:07 +08:00
return PathType . BEZIER ;
2020-10-12 17:04:28 +08:00
case 'L' :
2023-11-13 15:24:09 +08:00
return PathType . LINEAR ;
2020-10-12 17:04:28 +08:00
case 'P' :
2023-11-13 15:24:09 +08:00
return PathType . PERFECT_CURVE ;
2020-10-12 17:04:28 +08:00
}
}
/// <summary>
2020-10-12 18:16:37 +08:00
/// Converts a given point string into a set of path control points.
2020-10-12 17:04:28 +08:00
/// </summary>
2020-10-12 18:16:37 +08:00
/// <remarks>
/// A point string takes the form: X|1:1|2:2|2:2|3:3|Y|1:1|2:2.
/// This has three segments:
/// <list type="number">
/// <item>
/// <description>X: { (1,1), (2,2) } (implicit segment)</description>
/// </item>
/// <item>
/// <description>X: { (2,2), (3,3) } (implicit segment)</description>
/// </item>
/// <item>
/// <description>Y: { (3,3), (1,1), (2, 2) } (explicit segment)</description>
/// </item>
/// </list>
/// </remarks>
/// <param name="pointString">The point string.</param>
/// <param name="offset">The positional offset to apply to the control points.</param>
/// <returns>All control points in the resultant path.</returns>
private PathControlPoint [ ] convertPathString ( string pointString , Vector2 offset )
{
// This code takes on the responsibility of handling explicit segments of the path ("X" & "Y" from above). Implicit segments are handled by calls to convertPoints().
2024-02-29 20:02:04 +08:00
string [ ] pointStringSplit = pointString . Split ( '|' ) ;
2020-10-12 18:16:37 +08:00
2024-02-29 20:02:04 +08:00
var pointsBuffer = ArrayPool < Vector2 > . Shared . Rent ( pointStringSplit . Length ) ;
var segmentsBuffer = ArrayPool < ( PathType Type , int StartIndex ) > . Shared . Rent ( pointStringSplit . Length ) ;
int currentPointsIndex = 0 ;
int currentSegmentsIndex = 0 ;
2024-02-29 00:24:24 +08:00
2024-02-28 22:51:36 +08:00
try
2020-10-12 18:16:37 +08:00
{
2024-02-29 20:02:04 +08:00
foreach ( string s in pointStringSplit )
2024-02-28 22:01:39 +08:00
{
2024-02-28 22:51:36 +08:00
if ( char . IsLetter ( s [ 0 ] ) )
{
// The start of a new segment(indicated by having an alpha character at position 0).
var pathType = convertPathType ( s ) ;
2024-02-29 20:02:04 +08:00
segmentsBuffer [ currentSegmentsIndex + + ] = ( pathType , currentPointsIndex ) ;
2024-02-28 22:42:08 +08:00
2024-02-28 22:51:36 +08:00
// First segment is prepended by an extra zero point
2024-02-29 20:02:04 +08:00
if ( currentPointsIndex = = 0 )
pointsBuffer [ currentPointsIndex + + ] = Vector2 . Zero ;
2024-02-28 22:51:36 +08:00
}
else
{
2024-02-29 20:02:04 +08:00
pointsBuffer [ currentPointsIndex + + ] = readPoint ( s , offset ) ;
2024-02-28 22:51:36 +08:00
}
2024-02-28 22:01:39 +08:00
}
2024-02-28 22:51:36 +08:00
2024-02-29 20:02:04 +08:00
int pointsCount = currentPointsIndex ;
int segmentsCount = currentSegmentsIndex ;
2024-02-28 22:51:36 +08:00
var controlPoints = new List < ArraySegment < PathControlPoint > > ( pointsCount ) ;
2024-02-29 20:02:04 +08:00
var allPoints = new ArraySegment < Vector2 > ( pointsBuffer , 0 , pointsCount ) ;
2024-02-28 22:51:36 +08:00
for ( int i = 0 ; i < segmentsCount ; i + + )
2024-02-28 22:01:39 +08:00
{
2024-02-29 20:02:04 +08:00
if ( i < segmentsCount - 1 )
{
int startIndex = segmentsBuffer [ i ] . StartIndex ;
int endIndex = segmentsBuffer [ i + 1 ] . StartIndex ;
2024-03-19 07:43:34 +08:00
controlPoints . AddRange ( convertPoints ( segmentsBuffer [ i ] . Type , allPoints . Slice ( startIndex , endIndex - startIndex ) , pointsBuffer [ endIndex ] ) ) ;
2024-02-29 20:02:04 +08:00
}
else
{
int startIndex = segmentsBuffer [ i ] . StartIndex ;
2024-03-19 07:43:34 +08:00
controlPoints . AddRange ( convertPoints ( segmentsBuffer [ i ] . Type , allPoints . Slice ( startIndex ) , null ) ) ;
2024-02-29 20:02:04 +08:00
}
2024-02-28 22:01:39 +08:00
}
2020-10-12 18:16:37 +08:00
2024-03-19 07:43:34 +08:00
return mergeControlPointsLists ( controlPoints ) ;
2024-02-28 22:51:36 +08:00
}
finally
2024-02-28 22:01:39 +08:00
{
2024-02-29 20:02:04 +08:00
ArrayPool < Vector2 > . Shared . Return ( pointsBuffer ) ;
ArrayPool < ( PathType , int ) > . Shared . Return ( segmentsBuffer ) ;
2020-10-12 18:16:37 +08:00
}
2024-02-28 22:01:39 +08:00
static Vector2 readPoint ( string value , Vector2 startPos )
{
string [ ] vertexSplit = value . Split ( ':' ) ;
Vector2 pos = new Vector2 ( ( int ) Parsing . ParseDouble ( vertexSplit [ 0 ] , Parsing . MAX_COORDINATE_VALUE ) , ( int ) Parsing . ParseDouble ( vertexSplit [ 1 ] , Parsing . MAX_COORDINATE_VALUE ) ) - startPos ;
return pos ;
}
}
2020-10-12 18:16:37 +08:00
/// <summary>
/// Converts a given point list into a set of path segments.
/// </summary>
2024-02-28 22:42:08 +08:00
/// <param name="type">The path type of the point list.</param>
2020-10-12 18:16:37 +08:00
/// <param name="points">The point list.</param>
2020-10-12 17:04:28 +08:00
/// <param name="endPoint">Any extra endpoint to consider as part of the points. This will NOT be returned.</param>
2024-02-28 22:42:08 +08:00
/// <returns>The set of points contained by <paramref name="points"/> as one or more segments of the path.</returns>
2024-02-28 22:51:36 +08:00
private IEnumerable < ArraySegment < PathControlPoint > > convertPoints ( PathType type , ArraySegment < Vector2 > points , Vector2 ? endPoint )
2019-12-05 18:53:31 +08:00
{
2024-02-28 22:51:36 +08:00
var vertices = new PathControlPoint [ points . Count ] ;
2020-10-12 17:04:28 +08:00
// Parse into control points.
2024-02-28 22:51:36 +08:00
for ( int i = 0 ; i < points . Count ; i + + )
2024-02-28 22:42:08 +08:00
vertices [ i ] = new PathControlPoint { Position = points [ i ] } ;
2020-10-12 17:04:28 +08:00
2020-10-12 18:22:34 +08:00
// Edge-case rules (to match stable).
2023-11-13 15:24:09 +08:00
if ( type = = PathType . PERFECT_CURVE )
2019-12-05 18:53:31 +08:00
{
2024-04-03 01:50:39 +08:00
int endPointLength = endPoint = = null ? 0 : 1 ;
2024-02-28 22:42:08 +08:00
if ( vertices . Length + endPointLength ! = 3 )
2023-11-08 18:43:54 +08:00
type = PathType . BEZIER ;
2024-02-28 22:51:36 +08:00
else if ( isLinear ( points [ 0 ] , points [ 1 ] , endPoint ? ? points [ 2 ] ) )
2019-12-05 18:53:31 +08:00
{
// osu-stable special-cased colinear perfect curves to a linear path
2023-11-08 18:43:54 +08:00
type = PathType . LINEAR ;
2019-12-05 18:53:31 +08:00
}
}
2020-10-12 18:22:34 +08:00
// The first control point must have a definite type.
2021-08-26 00:42:57 +08:00
vertices [ 0 ] . Type = type ;
2019-12-05 18:53:31 +08:00
2020-10-12 18:22:34 +08:00
// A path can have multiple implicit segments of the same type if there are two sequential control points with the same position.
2020-10-12 18:16:37 +08:00
// To handle such cases, this code may return multiple path segments with the final control point in each segment having a non-null type.
2020-10-12 18:22:34 +08:00
// For the point string X|1:1|2:2|2:2|3:3, this code returns the segments:
// X: { (1,1), (2, 2) }
// X: { (3, 3) }
// Note: (2, 2) is not returned in the second segments, as it is implicit in the path.
2020-10-12 18:16:37 +08:00
int startIndex = 0 ;
int endIndex = 0 ;
2024-02-28 22:42:08 +08:00
while ( + + endIndex < vertices . Length )
2019-12-05 18:53:31 +08:00
{
2022-05-18 16:11:08 +08:00
// Keep incrementing while an implicit segment doesn't need to be started.
2021-08-26 00:42:57 +08:00
if ( vertices [ endIndex ] . Position ! = vertices [ endIndex - 1 ] . Position )
2020-10-12 18:16:37 +08:00
continue ;
2023-11-08 18:43:54 +08:00
// Legacy CATMULL sliders don't support multiple segments, so adjacent CATMULL segments should be treated as a single one.
2022-05-24 10:47:42 +08:00
// Importantly, this is not applied to the first control point, which may duplicate the slider path's position
// resulting in a duplicate (0,0) control point in the resultant list.
2023-11-08 18:43:54 +08:00
if ( type = = PathType . CATMULL & & endIndex > 1 & & FormatVersion < LegacyBeatmapEncoder . FIRST_LAZER_VERSION )
2022-05-18 16:11:08 +08:00
continue ;
2021-04-05 16:49:36 +08:00
// The last control point of each segment is not allowed to start a new implicit segment.
2024-02-28 22:42:08 +08:00
if ( endIndex = = vertices . Length - 1 )
2021-04-05 16:49:36 +08:00
continue ;
2020-10-12 18:16:37 +08:00
// Force a type on the last point, and return the current control point set as a segment.
2021-08-26 00:42:57 +08:00
vertices [ endIndex - 1 ] . Type = type ;
2024-02-28 22:51:36 +08:00
yield return new ArraySegment < PathControlPoint > ( vertices , startIndex , endIndex - startIndex ) ;
2020-10-12 18:16:37 +08:00
// Skip the current control point - as it's the same as the one that's just been returned.
startIndex = endIndex + 1 ;
2020-10-12 17:04:28 +08:00
}
2019-12-05 18:53:31 +08:00
2024-02-28 22:51:36 +08:00
if ( startIndex < endIndex )
yield return new ArraySegment < PathControlPoint > ( vertices , startIndex , endIndex - startIndex ) ;
2019-12-05 18:53:31 +08:00
2024-02-28 22:51:36 +08:00
static bool isLinear ( Vector2 p0 , Vector2 p1 , Vector2 p2 )
= > Precision . AlmostEquals ( 0 , ( p1 . Y - p0 . Y ) * ( p2 . X - p0 . X )
2024-02-29 10:47:16 +08:00
- ( p1 . X - p0 . X ) * ( p2 . Y - p0 . Y ) ) ;
2019-12-05 18:53:31 +08:00
}
2024-03-19 07:43:34 +08:00
private PathControlPoint [ ] mergeControlPointsLists ( List < ArraySegment < PathControlPoint > > controlPointList )
2024-02-29 00:07:00 +08:00
{
int totalCount = 0 ;
foreach ( var arr in controlPointList )
totalCount + = arr . Count ;
var mergedArray = new PathControlPoint [ totalCount ] ;
int copyIndex = 0 ;
foreach ( var arr in controlPointList )
{
arr . AsSpan ( ) . CopyTo ( mergedArray . AsSpan ( copyIndex ) ) ;
copyIndex + = arr . Count ;
}
return mergedArray ;
}
2017-04-18 08:13:36 +08:00
/// <summary>
/// Creates a legacy Hit-type hit object.
/// </summary>
/// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
2018-08-15 09:48:42 +08:00
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
2017-04-18 08:13:36 +08:00
/// <returns>The hit object.</returns>
2018-08-15 09:48:42 +08:00
protected abstract HitObject CreateHit ( Vector2 position , bool newCombo , int comboOffset ) ;
2018-04-13 17:19:50 +08:00
2017-04-18 08:13:36 +08:00
/// <summary>
/// Creats a legacy Slider-type hit object.
/// </summary>
/// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
2018-08-15 09:48:42 +08:00
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
2017-04-18 08:13:36 +08:00
/// <param name="controlPoints">The slider control points.</param>
/// <param name="length">The slider length.</param>
/// <param name="repeatCount">The slider repeat count.</param>
2018-10-16 16:10:24 +08:00
/// <param name="nodeSamples">The samples to be played when the slider nodes are hit. This includes the head and tail of the slider.</param>
2017-04-18 08:13:36 +08:00
/// <returns>The hit object.</returns>
2019-12-05 18:53:31 +08:00
protected abstract HitObject CreateSlider ( Vector2 position , bool newCombo , int comboOffset , PathControlPoint [ ] controlPoints , double? length , int repeatCount ,
2021-10-23 16:59:07 +08:00
IList < IList < HitSampleInfo > > nodeSamples ) ;
2018-04-13 17:19:50 +08:00
2017-04-18 08:13:36 +08:00
/// <summary>
/// Creates a legacy Spinner-type hit object.
/// </summary>
/// <param name="position">The position of the hit object.</param>
2018-08-15 09:48:42 +08:00
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
2020-05-27 11:37:44 +08:00
/// <param name="duration">The spinner duration.</param>
2017-04-18 08:13:36 +08:00
/// <returns>The hit object.</returns>
2020-05-27 11:37:44 +08:00
protected abstract HitObject CreateSpinner ( Vector2 position , bool newCombo , int comboOffset , double duration ) ;
2018-04-13 17:19:50 +08:00
2017-05-12 15:35:57 +08:00
/// <summary>
/// Creates a legacy Hold-type hit object.
/// </summary>
/// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
2018-08-15 09:48:42 +08:00
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
2020-05-31 21:39:03 +08:00
/// <param name="duration">The hold duration.</param>
2020-05-27 11:37:44 +08:00
protected abstract HitObject CreateHold ( Vector2 position , bool newCombo , int comboOffset , double duration ) ;
2018-04-13 17:19:50 +08:00
2019-12-10 19:04:37 +08:00
private List < HitSampleInfo > convertSoundType ( LegacyHitSoundType type , SampleBankInfo bankInfo )
2017-04-21 12:51:40 +08:00
{
2023-02-10 05:18:12 +08:00
var soundTypes = new List < HitSampleInfo > ( ) ;
if ( string . IsNullOrEmpty ( bankInfo . Filename ) )
2019-03-05 17:21:29 +08:00
{
2023-02-10 05:18:12 +08:00
soundTypes . Add ( new LegacyHitSampleInfo ( HitSampleInfo . HIT_NORMAL , bankInfo . BankForNormal , bankInfo . Volume , bankInfo . CustomSampleBank ,
2023-05-16 15:29:24 +08:00
// if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
// None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
type ! = LegacyHitSoundType . None & & ! type . HasFlagFast ( LegacyHitSoundType . Normal ) ) ) ;
2019-03-05 17:21:29 +08:00
}
2023-02-10 05:18:12 +08:00
else
2017-04-21 12:51:40 +08:00
{
2023-02-10 05:18:12 +08:00
// Todo: This should set the normal SampleInfo if the specified sample file isn't found, but that's a pretty edge-case scenario
soundTypes . Add ( new FileHitSampleInfo ( bankInfo . Filename , bankInfo . Volume ) ) ;
}
2018-04-13 17:19:50 +08:00
2021-02-25 14:38:56 +08:00
if ( type . HasFlagFast ( LegacyHitSoundType . Finish ) )
2022-10-25 13:55:33 +08:00
soundTypes . Add ( new LegacyHitSampleInfo ( HitSampleInfo . HIT_FINISH , bankInfo . BankForAdditions , bankInfo . Volume , bankInfo . CustomSampleBank ) ) ;
2018-04-13 17:19:50 +08:00
2021-02-25 14:38:56 +08:00
if ( type . HasFlagFast ( LegacyHitSoundType . Whistle ) )
2022-10-25 13:55:33 +08:00
soundTypes . Add ( new LegacyHitSampleInfo ( HitSampleInfo . HIT_WHISTLE , bankInfo . BankForAdditions , bankInfo . Volume , bankInfo . CustomSampleBank ) ) ;
2018-04-13 17:19:50 +08:00
2021-02-25 14:38:56 +08:00
if ( type . HasFlagFast ( LegacyHitSoundType . Clap ) )
2022-10-25 13:55:33 +08:00
soundTypes . Add ( new LegacyHitSampleInfo ( HitSampleInfo . HIT_CLAP , bankInfo . BankForAdditions , bankInfo . Volume , bankInfo . CustomSampleBank ) ) ;
2018-04-13 17:19:50 +08:00
2017-04-21 12:51:40 +08:00
return soundTypes ;
}
2018-04-13 17:19:50 +08:00
2017-04-21 13:36:28 +08:00
private class SampleBankInfo
{
2022-10-25 13:55:33 +08:00
/// <summary>
/// An optional overriding filename which causes all bank/sample specifications to be ignored.
/// </summary>
2018-07-02 13:20:35 +08:00
public string Filename ;
2022-10-25 13:55:33 +08:00
/// <summary>
/// The bank identifier to use for the base ("hitnormal") sample.
/// Transferred to <see cref="HitSampleInfo.Bank"/> when appropriate.
/// </summary>
2023-05-16 15:29:24 +08:00
[CanBeNull]
2022-10-25 13:55:33 +08:00
public string BankForNormal ;
/// <summary>
/// The bank identifier to use for additions ("hitwhistle", "hitfinish", "hitclap").
/// Transferred to <see cref="HitSampleInfo.Bank"/> when appropriate.
/// </summary>
2023-05-16 15:29:24 +08:00
[CanBeNull]
2022-10-25 13:55:33 +08:00
public string BankForAdditions ;
/// <summary>
/// Hit sample volume (0-100).
/// See <see cref="HitSampleInfo.Volume"/>.
/// </summary>
2017-04-21 18:51:23 +08:00
public int Volume ;
2018-04-13 17:19:50 +08:00
2022-10-25 13:55:33 +08:00
/// <summary>
/// The index of the custom sample bank. Is only used if 2 or above for "reasons".
/// This will add a suffix to lookups, allowing extended bank lookups (ie. "normal-hitnormal-2").
/// See <see cref="HitSampleInfo.Suffix"/>.
/// </summary>
2018-07-20 14:12:44 +08:00
public int CustomSampleBank ;
2018-07-02 13:20:35 +08:00
public SampleBankInfo Clone ( ) = > ( SampleBankInfo ) MemberwiseClone ( ) ;
}
2020-12-01 14:37:51 +08:00
#nullable enable
2020-12-01 14:44:16 +08:00
public class LegacyHitSampleInfo : HitSampleInfo , IEquatable < LegacyHitSampleInfo >
2018-07-20 14:12:44 +08:00
{
2020-12-01 14:37:51 +08:00
public readonly int CustomSampleBank ;
2020-06-21 22:43:21 +08:00
/// <summary>
/// Whether this hit sample is layered.
/// </summary>
/// <remarks>
/// Layered hit samples are automatically added in all modes (except osu!mania), but can be disabled
2021-10-22 13:41:59 +08:00
/// using the <see cref="SkinConfiguration.LegacySetting.LayeredHitSounds"/> skin config option.
2020-06-21 22:43:21 +08:00
/// </remarks>
2020-12-01 14:37:51 +08:00
public readonly bool IsLayered ;
2023-05-16 15:29:24 +08:00
/// <summary>
/// Whether a bank was specified locally to the relevant hitobject.
/// If <c>false</c>, a bank will be retrieved from the closest control point.
/// </summary>
public bool BankSpecified ;
2020-12-01 17:09:28 +08:00
public LegacyHitSampleInfo ( string name , string? bank = null , int volume = 0 , int customSampleBank = 0 , bool isLayered = false )
2023-05-16 15:29:24 +08:00
: base ( name , bank ? ? SampleControlPoint . DEFAULT_BANK , customSampleBank > = 2 ? customSampleBank . ToString ( ) : null , volume )
2020-12-01 14:37:51 +08:00
{
CustomSampleBank = customSampleBank ;
2023-05-16 15:29:24 +08:00
BankSpecified = ! string . IsNullOrEmpty ( bank ) ;
2020-12-01 14:37:51 +08:00
IsLayered = isLayered ;
}
2023-05-17 13:11:14 +08:00
public sealed override HitSampleInfo With ( Optional < string > newName = default , Optional < string > newBank = default , Optional < string? > newSuffix = default , Optional < int > newVolume = default )
2020-12-02 09:55:48 +08:00
= > With ( newName , newBank , newVolume ) ;
2020-12-01 14:37:51 +08:00
2023-05-17 13:11:14 +08:00
public virtual LegacyHitSampleInfo With ( Optional < string > newName = default , Optional < string > newBank = default , Optional < int > newVolume = default ,
2022-10-25 13:55:33 +08:00
Optional < int > newCustomSampleBank = default ,
2020-12-02 09:55:48 +08:00
Optional < bool > newIsLayered = default )
2023-05-17 13:11:14 +08:00
= > new LegacyHitSampleInfo ( newName . GetOr ( Name ) , newBank . GetOr ( Bank ) , newVolume . GetOr ( Volume ) , newCustomSampleBank . GetOr ( CustomSampleBank ) , newIsLayered . GetOr ( IsLayered ) ) ;
2020-12-01 14:44:16 +08:00
public bool Equals ( LegacyHitSampleInfo ? other )
2022-03-23 19:18:44 +08:00
// The additions to equality checks here are *required* to ensure that pooling works correctly.
// Of note, `IsLayered` may cause the usage of `SampleVirtual` instead of an actual sample (in cases playback is not required).
// Removing it would cause samples which may actually require playback to potentially source for a `SampleVirtual` sample pool.
2022-03-23 19:15:17 +08:00
= > base . Equals ( other ) & & CustomSampleBank = = other . CustomSampleBank & & IsLayered = = other . IsLayered ;
2020-12-01 14:44:16 +08:00
2020-12-01 17:09:21 +08:00
public override bool Equals ( object? obj )
= > obj is LegacyHitSampleInfo other & & Equals ( other ) ;
2020-12-01 14:44:16 +08:00
2022-03-23 19:15:13 +08:00
public override int GetHashCode ( ) = > HashCode . Combine ( base . GetHashCode ( ) , CustomSampleBank , IsLayered ) ;
2018-07-20 14:12:44 +08:00
}
2020-12-01 14:44:16 +08:00
private class FileHitSampleInfo : LegacyHitSampleInfo , IEquatable < FileHitSampleInfo >
2018-07-02 13:20:35 +08:00
{
2020-12-01 14:37:51 +08:00
public readonly string Filename ;
2018-07-02 13:20:35 +08:00
2020-12-01 14:37:51 +08:00
public FileHitSampleInfo ( string filename , int volume )
// Force CSS=1 to make sure that the LegacyBeatmapSkin does not fall back to the user skin.
2020-04-14 20:05:07 +08:00
// Note that this does not change the lookup names, as they are overridden locally.
2020-12-01 14:37:51 +08:00
: base ( string . Empty , customSampleBank : 1 , volume : volume )
{
Filename = filename ;
2020-04-13 19:09:17 +08:00
}
2018-07-02 13:20:35 +08:00
public override IEnumerable < string > LookupNames = > new [ ]
2017-04-21 13:36:28 +08:00
{
2018-07-02 13:20:35 +08:00
Filename ,
Path . ChangeExtension ( Filename , null )
} ;
2020-12-01 14:37:51 +08:00
2023-05-17 13:11:14 +08:00
public sealed override LegacyHitSampleInfo With ( Optional < string > newName = default , Optional < string > newBank = default , Optional < int > newVolume = default ,
2022-10-25 13:55:33 +08:00
Optional < int > newCustomSampleBank = default ,
2020-12-02 09:55:48 +08:00
Optional < bool > newIsLayered = default )
= > new FileHitSampleInfo ( Filename , newVolume . GetOr ( Volume ) ) ;
2020-12-01 14:44:16 +08:00
public bool Equals ( FileHitSampleInfo ? other )
= > base . Equals ( other ) & & Filename = = other . Filename ;
public override bool Equals ( object? obj )
2020-12-01 17:09:21 +08:00
= > obj is FileHitSampleInfo other & & Equals ( other ) ;
2020-12-01 14:44:16 +08:00
public override int GetHashCode ( ) = > HashCode . Combine ( base . GetHashCode ( ) , Filename ) ;
2017-04-21 13:36:28 +08:00
}
2016-11-14 21:03:39 +08:00
}
}