public LaneHoldStartNote(LaneHoldStartNote other) : base(other) { }
/// <summary> /// Parses the sm/ssc string representation of measures and notes as /// events into the given Chart. /// </summary> /// <param name="chart">Chart to parse notes into.</param> /// <param name="notesStr">String representation of notes from an sm or ssc chart.</param> /// <returns>Whether the notes represent a valid chart or not.</returns> protected bool ParseNotes(Chart chart, string notesStr) { if (chart.NumInputs < 1) { Logger?.Warn( $"Cannot parse notes for {chart.Type} {chart.DifficultyType} Chart. Unknown number of inputs. This Chart will be ignored."); return(false); } var validChart = true; var player = 0; var measure = 0; var currentMeasureEvents = new List <Event>(); notesStr = notesStr.Trim(SMCommon.SMAllWhiteSpace); var notesStrsPerPlayer = notesStr.Split('&'); foreach (var notesStrForPlayer in notesStrsPerPlayer) { var holding = new bool[chart.NumInputs]; var rolling = new bool[chart.NumInputs]; // RemoveEmptyEntries seems wrong, but matches Stepmania parsing logic. var measures = notesStrForPlayer.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (var measureStr in measures) { var lines = measureStr.Trim(SMCommon.SMAllWhiteSpace) .Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); var linesInMeasure = lines.Length; var lineInMeasure = 0; foreach (var line in lines) { var trimmedLine = line.Trim(SMCommon.SMAllWhiteSpace); // Parse this line as note data for (int charIndex = 0, laneIndex = 0; charIndex < trimmedLine.Length && laneIndex < chart.NumInputs; charIndex++, laneIndex++) { // Get the note type. var c = trimmedLine[charIndex]; var noteType = SMCommon.NoteType.None; var noteString = SMCommon.NoteStrings[(int)noteType]; for (var i = 0; i < SMCommon.NoteChars.Length; i++) { if (c == SMCommon.NoteChars[i]) { noteType = (SMCommon.NoteType)i; noteString = SMCommon.NoteStrings[i]; break; } } // Validation. if (noteType == SMCommon.NoteType.Tap || noteType == SMCommon.NoteType.Mine || noteType == SMCommon.NoteType.Lift || noteType == SMCommon.NoteType.Fake || noteType == SMCommon.NoteType.KeySound || noteType == SMCommon.NoteType.HoldStart || noteType == SMCommon.NoteType.RollStart) { if (holding[laneIndex]) { Logger?.Error( $"Invalid {chart.Type} {chart.DifficultyType} Chart. {noteString} during hold on lane {laneIndex} during measure {measure}. This Chart will be ignored."); validChart = false; } if (rolling[laneIndex]) { Logger?.Error( $"Invalid {chart.Type} {chart.DifficultyType} Chart. {noteString} during roll on lane {laneIndex} during measure {measure}. This Chart will be ignored."); validChart = false; } } else if (noteType == SMCommon.NoteType.HoldEnd) { if (!holding[laneIndex] && !rolling[laneIndex]) { Logger?.Error( $"Invalid {chart.Type} {chart.DifficultyType} Chart. {noteString} while neither holding nor rolling on lane {laneIndex} during measure {measure}. This Chart will be ignored."); validChart = false; } } if (!validChart) { break; } // Create a LaneNote based on the note type. LaneNote note = null; if (noteType == SMCommon.NoteType.Tap || noteType == SMCommon.NoteType.Fake || noteType == SMCommon.NoteType.Lift) { note = new LaneTapNote { SourceType = c.ToString() }; } else if (noteType == SMCommon.NoteType.Mine || noteType == SMCommon.NoteType.KeySound) { note = new LaneNote { SourceType = c.ToString() }; } else if (noteType == SMCommon.NoteType.HoldStart) { holding[laneIndex] = true; note = new LaneHoldStartNote { SourceType = c.ToString() }; } else if (noteType == SMCommon.NoteType.RollStart) { rolling[laneIndex] = true; note = new LaneHoldStartNote { SourceType = c.ToString() }; } else if (noteType == SMCommon.NoteType.HoldEnd) { note = new LaneHoldEndNote { SourceType = c.ToString() }; holding[laneIndex] = false; rolling[laneIndex] = false; } // Keysound parsing. // TODO: Parse keysounds properly. For now, putting them in SourceExtras. if (charIndex + 1 < trimmedLine.Length && trimmedLine[charIndex + 1] == '[') { var startIndex = charIndex + 1; while (charIndex < trimmedLine.Length) { if (trimmedLine[charIndex] == ']') { break; } charIndex++; } var endIndex = charIndex - 1; if (endIndex > startIndex && note != null) { if (int.TryParse(trimmedLine.Substring(startIndex, endIndex - startIndex), out var keySoundIndex)) { note.Extras.AddSourceExtra(SMCommon.TagFumenKeySoundIndex, keySoundIndex, true); } } } // Deprecated Attack parsing if (charIndex + 1 < trimmedLine.Length && trimmedLine[charIndex + 1] == '{') { while (charIndex < trimmedLine.Length) { if (trimmedLine[charIndex] == '}') { break; } charIndex++; } } // Deprecated Item parsing if (charIndex + 1 < trimmedLine.Length && trimmedLine[charIndex + 1] == '<') { while (charIndex < trimmedLine.Length) { if (trimmedLine[charIndex] == '>') { break; } charIndex++; } } // No note at this position, continue. if (null == note) { continue; } // Configure common parameters on the note and add it. var beat = (lineInMeasure * SMCommon.NumBeatsPerMeasure) / linesInMeasure; var subDivisionNumerator = lineInMeasure * SMCommon.NumBeatsPerMeasure - linesInMeasure * beat; var subDivisionDenominator = linesInMeasure; note.Lane = laneIndex; note.Player = player; note.Position = new MetricPosition(measure, beat, subDivisionNumerator, subDivisionDenominator); currentMeasureEvents.Add(note); } if (!validChart) { break; } // Advance line marker lineInMeasure++; } if (!validChart) { break; } chart.Layers[0].Events.AddRange(currentMeasureEvents); currentMeasureEvents.Clear(); measure++; } // Validation. for (var i = 0; i < chart.NumInputs; i++) { if (holding[i]) { Logger?.Error( $"Invalid {chart.Type} {chart.DifficultyType} Chart. Incomplete hold on lane {i}. This Chart will be ignored."); validChart = false; } if (rolling[i]) { Logger?.Error( $"Invalid {chart.Type} {chart.DifficultyType} Chart. Incomplete roll on lane {i}. This Chart will be ignored."); validChart = false; } } if (!validChart) { break; } player++; } return(validChart); }