/// <summary>
        /// Converts specified custom sample info into a list of sample infos that will be used in the game play.
        /// </summary>
        private List <SoundInfo> GetSamples(SoundType type, CustomSampleInfo customInfo)
        {
            // Include normal sound as default
            var samples = new List <SoundInfo>()
            {
                new LegacySampleInfo()
                {
                    Sample        = customInfo.NormalSample,
                    Sound         = SoundInfo.HitNormal,
                    Volume        = customInfo.Volume,
                    VariantNumber = customInfo.Variant
                }
            };

            if ((type & SoundType.Finish) != 0)
            {
                samples.Add(new LegacySampleInfo()
                {
                    Sample        = customInfo.AdditionalSample,
                    Sound         = SoundInfo.HitFinish,
                    Volume        = customInfo.Volume,
                    VariantNumber = customInfo.Variant
                });
            }

            if ((type & SoundType.Whistle) != 0)
            {
                samples.Add(new LegacySampleInfo()
                {
                    Sample        = customInfo.AdditionalSample,
                    Sound         = SoundInfo.HitWhistle,
                    Volume        = customInfo.Volume,
                    VariantNumber = customInfo.Variant
                });
            }

            if ((type & SoundType.Clap) != 0)
            {
                samples.Add(new LegacySampleInfo()
                {
                    Sample        = customInfo.AdditionalSample,
                    Sound         = SoundInfo.HitClap,
                    Volume        = customInfo.Volume,
                    VariantNumber = customInfo.Variant
                });
            }

            return(samples);
        }
        /// <summary>
        /// Parses custom sample information to support per-object sound variations.
        /// </summary>
        private void ParseCustomSample(string text, CustomSampleInfo info)
        {
            if (string.IsNullOrEmpty(text))
            {
                return;
            }

            string[] splits = text.Split(':');

            var normalType     = (OsuBeatmapDecoder.SampleType)ParseUtils.ParseInt(splits[0]);
            var additionalType = (OsuBeatmapDecoder.SampleType)ParseUtils.ParseInt(splits[1]);

            string normalTypeStr = normalType.ToString().ToLowerInvariant();

            if (normalTypeStr == "none")
            {
                normalTypeStr = null;
            }
            string additionalTypeStr = additionalType.ToString().ToLowerInvariant();

            if (additionalTypeStr == "none")
            {
                additionalTypeStr = null;
            }

            info.NormalSample     = normalTypeStr;
            info.AdditionalSample = string.IsNullOrEmpty(additionalTypeStr) ? normalTypeStr : additionalTypeStr;

            if (splits.Length > 2)
            {
                info.Variant = ParseUtils.ParseInt(splits[2]);
            }
            if (splits.Length > 3)
            {
                info.Volume = ParseUtils.ParseFloat(splits[3]) / 100f;
            }
            info.FileName = splits.Length > 4 ? splits[4] : null;
        }
        public BaseHitObject Parse(string text)
        {
            try
            {
                string[] splits = text.Split(',');

                Vector2       pos       = new Vector2(ParseUtils.ParseFloat(splits[0]), ParseUtils.ParseFloat(splits[1]));
                float         startTime = ParseUtils.ParseFloat(splits[2]) + offset;
                HitObjectType type      = (HitObjectType)ParseUtils.ParseInt(splits[3]);

                int comboOffset = (int)(type & HitObjectType.ComboOffset) >> 4;
                type &= ~HitObjectType.ComboOffset;

                bool isNewCombo = (int)(type & HitObjectType.NewCombo) != 0;
                type &= ~HitObjectType.NewCombo;

                var soundType    = (SoundType)ParseUtils.ParseInt(splits[4]);
                var customSample = new CustomSampleInfo();

                // Now parse the actual hit objects.
                BaseHitObject result = null;
                // If this object is a hit circle
                if ((type & HitObjectType.Circle) != 0)
                {
                    result = CreateCircle(pos, isNewCombo, comboOffset);

                    if (splits.Length > 5)
                    {
                        ParseCustomSample(splits[5], customSample);
                    }
                }
                else if ((type & HitObjectType.Slider) != 0)
                {
                    PathType pathType    = PathType.Catmull;
                    float    length      = 0;
                    string[] pointSplits = splits[5].Split('|');

                    // Find the number of valid slider node points.
                    int pointCount = 1;
                    foreach (var p in pointSplits)
                    {
                        if (p.Length > 1)
                        {
                            pointCount++;
                        }
                    }

                    // Parse node points
                    var nodePoints = new Vector2[pointCount];
                    nodePoints[0] = Vector2.zero;

                    int pointIndex = 1;
                    foreach (var p in pointSplits)
                    {
                        // Determine which path type was found.
                        if (p.Length == 1)
                        {
                            switch (p)
                            {
                            case "C":
                                pathType = PathType.Catmull;
                                break;

                            case "B":
                                pathType = PathType.Bezier;
                                break;

                            case "L":
                                pathType = PathType.Linear;
                                break;

                            case "P":
                                pathType = PathType.PerfectCurve;
                                break;
                            }
                            continue;
                        }
                        // Parse point position
                        string[] pointPos = p.Split(':');
                        nodePoints[pointIndex++] = new Vector2(ParseUtils.ParseFloat(pointPos[0]), ParseUtils.ParseFloat(pointPos[1])) - pos;
                    }

                    // Change perfect curve to linear if certain conditions meet.
                    if (nodePoints.Length == 3 && pathType == PathType.PerfectCurve && IsLinearPerfectCurve(nodePoints))
                    {
                        pathType = PathType.Linear;
                    }

                    // Parse slider repeat count
                    int repeatCount = ParseUtils.ParseInt(splits[6]);
                    if (repeatCount > 9000)
                    {
                        throw new Exception();
                    }
                    // Osu file has +1 addition to the actual number of repeats.
                    repeatCount = Math.Max(0, repeatCount - 1);

                    if (splits.Length > 7)
                    {
                        length = Math.Max(0, ParseUtils.ParseFloat(splits[7]));
                    }

                    if (splits.Length > 10)
                    {
                        ParseCustomSample(splits[10], customSample);
                    }

                    // Number of repeats + start(1) + end(1)
                    int nodeCount = repeatCount + 2;

                    // Parse per-node sound samples
                    var nodeCustomSamples = new List <CustomSampleInfo>();
                    for (int i = 0; i < nodeCount; i++)
                    {
                        nodeCustomSamples.Add(customSample.Clone());
                    }

                    if (splits.Length > 9 && splits[9].Length > 0)
                    {
                        string[] sets = splits[9].Split('|');
                        for (int i = 0; i < nodeCount; i++)
                        {
                            if (i >= sets.Length)
                            {
                                break;
                            }

                            ParseCustomSample(sets[i], nodeCustomSamples[i]);
                        }
                    }

                    // Set all nodes' sample types to default.
                    var nodeSampleTypes = new List <SoundType>();
                    for (int i = 0; i < nodeCount; i++)
                    {
                        nodeSampleTypes.Add(soundType);
                    }

                    // Parse per-node sample types
                    if (splits.Length > 8 && splits[8].Length > 0)
                    {
                        string[] nodeSampleSplits = splits[8].Split('|');
                        for (int i = 0; i < nodeCount; i++)
                        {
                            if (i > nodeSampleSplits.Length)
                            {
                                break;
                            }

                            nodeSampleTypes[i] = (SoundType)ParseUtils.ParseInt(nodeSampleSplits[i]);
                        }
                    }

                    // Map sample types to custom sample infos.
                    var nodeSamples = new List <List <SoundInfo> >(nodeCount);
                    for (int i = 0; i < nodeCount; i++)
                    {
                        nodeSamples.Add(GetSamples(nodeSampleTypes[i], nodeCustomSamples[i]));
                    }

                    result = CreateSlider(pos, isNewCombo, comboOffset, nodePoints, length, pathType, repeatCount, nodeSamples);
                    // Hit sound for the root slider should be played at the end.
                    result.Samples = nodeSamples[nodeSamples.Count - 1];
                }
                else if ((type & HitObjectType.Spinner) != 0)
                {
                    float endTime = Math.Max(startTime, ParseUtils.ParseFloat(splits[5]) + offset);
                    result = CreateSpinner(pos, isNewCombo, comboOffset, endTime);
                    if (splits.Length > 6)
                    {
                        ParseCustomSample(splits[6], customSample);
                    }
                }
                else if ((type & HitObjectType.Hold) != 0)
                {
                    float endTime = Math.Max(startTime, ParseUtils.ParseFloat(splits[2] + offset));

                    // I can understand all others except this, because Hold type only exists for Mania mode.
                    if (splits.Length > 5 && !string.IsNullOrEmpty(splits[5]))
                    {
                        string[] sampleSplits = splits[5].Split(':');
                        endTime = Math.Max(startTime, ParseUtils.ParseFloat(sampleSplits[0]));
                        ParseCustomSample(string.Join(":", sampleSplits.Skip(1).ToArray()), customSample);
                    }

                    result = CreateHold(pos, isNewCombo, comboOffset, endTime + offset);
                }

                if (result == null)
                {
                    Logger.LogVerbose("HitObjectParser.Parse - Unknown hit object for line: " + text);
                    return(null);
                }

                result.StartTime = startTime;
                if (result.Samples.Count == 0)
                {
                    result.Samples = GetSamples(soundType, customSample);
                }
                isFirstObject = false;
                return(result);
            }
            catch (Exception e)
            {
                Logger.LogError($"HitObjectParser.Parse - Failed to parse line: {text}, Error: {e.Message}");
            }
            return(null);
        }