protected static bool TryGetChartType(Chart chart, out SMCommon.ChartType chartType) { // If the Chart's Type is already a supported ChartType, use that. if (SMCommon.TryGetChartType(chart.Type, out chartType)) { return(true); } // Otherwise infer the ChartType from the number of inputs and players chartType = SMCommon.ChartType.dance_single; if (chart.NumPlayers == 1) { if (chart.NumInputs <= 3) { chartType = SMCommon.ChartType.dance_threepanel; return(true); } if (chart.NumInputs == 4) { chartType = SMCommon.ChartType.dance_single; return(true); } if (chart.NumInputs <= 6) { chartType = SMCommon.ChartType.dance_solo; return(true); } if (chart.NumInputs <= 8) { chartType = SMCommon.ChartType.dance_double; return(true); } } if (chart.NumPlayers == 2) { if (chart.NumInputs <= 8) { chartType = SMCommon.ChartType.dance_routine; return(true); } } return(false); }
public override bool Parse(MSDFile.Value value) { // Only consider this line if it matches this property name. if (!ParseFirstParameter(value, out var type)) { return(false); } Chart.Type = type; // Parse chart type and set number of players and inputs. if (!SMCommon.TryGetChartType(Chart.Type, out var smChartType)) { Chart.Type = null; Logger?.Error($"{PropertyName}: Failed to parse {SMCommon.TagStepsType} value '{smChartType}'. This chart will be ignored."); return(true); } Chart.NumPlayers = SMCommon.Properties[(int)smChartType].NumPlayers; Chart.NumInputs = SMCommon.Properties[(int)smChartType].NumInputs; Chart.Extras.AddSourceExtra(SMCommon.TagStepsType, Chart.Type, true); return(true); }
/// <summary> /// Load the sm file specified by the provided file path. /// </summary> public override async Task <Song> Load() { // Load the file as an MSDFile. var msdFile = new MSDFile(); var result = await msdFile.Load(FilePath); if (!result) { Logger.Error("Failed to load MSD File."); return(null); } var tempos = new Dictionary <double, double>(); var stops = new Dictionary <double, double>(); var song = new Song(); song.SourceType = FileFormatType.SM; var propertyParsers = new Dictionary <string, PropertyParser>() { [SMCommon.TagTitle] = new PropertyToSongPropertyParser(SMCommon.TagTitle, nameof(Song.Title), song), [SMCommon.TagSubtitle] = new PropertyToSongPropertyParser(SMCommon.TagSubtitle, nameof(Song.SubTitle), song), [SMCommon.TagArtist] = new PropertyToSongPropertyParser(SMCommon.TagArtist, nameof(Song.Artist), song), [SMCommon.TagTitleTranslit] = new PropertyToSongPropertyParser(SMCommon.TagTitleTranslit, nameof(Song.TitleTransliteration), song), [SMCommon.TagSubtitleTranslit] = new PropertyToSongPropertyParser(SMCommon.TagSubtitleTranslit, nameof(Song.SubTitleTransliteration), song), [SMCommon.TagArtistTranslit] = new PropertyToSongPropertyParser(SMCommon.TagArtistTranslit, nameof(Song.ArtistTransliteration), song), [SMCommon.TagGenre] = new PropertyToSongPropertyParser(SMCommon.TagGenre, nameof(Song.Genre), song), [SMCommon.TagCredit] = new PropertyToSourceExtrasParser <string>(SMCommon.TagCredit, song.Extras), [SMCommon.TagBanner] = new PropertyToSongPropertyParser(SMCommon.TagBanner, nameof(Song.SongSelectImage), song), [SMCommon.TagBackground] = new PropertyToSourceExtrasParser <string>(SMCommon.TagBackground, song.Extras), [SMCommon.TagLyricsPath] = new PropertyToSourceExtrasParser <string>(SMCommon.TagLyricsPath, song.Extras), [SMCommon.TagCDTitle] = new PropertyToSourceExtrasParser <string>(SMCommon.TagCDTitle, song.Extras), [SMCommon.TagMusic] = new PropertyToSourceExtrasParser <string>(SMCommon.TagMusic, song.Extras), [SMCommon.TagOffset] = new PropertyToSourceExtrasParser <double>(SMCommon.TagOffset, song.Extras), [SMCommon.TagBPMs] = new CSVListAtTimePropertyParser <double>(SMCommon.TagBPMs, tempos, song.Extras, SMCommon.TagFumenRawBpmsStr), [SMCommon.TagStops] = new CSVListAtTimePropertyParser <double>(SMCommon.TagStops, stops, song.Extras, SMCommon.TagFumenRawStopsStr), [SMCommon.TagFreezes] = new CSVListAtTimePropertyParser <double>(SMCommon.TagFreezes, stops), [SMCommon.TagDelays] = new PropertyToSourceExtrasParser <string>(SMCommon.TagDelays, song.Extras), // Removed, see https://github.com/stepmania/stepmania/issues/9 [SMCommon.TagTimeSignatures] = new PropertyToSourceExtrasParser <string>(SMCommon.TagTimeSignatures, song.Extras), [SMCommon.TagTickCounts] = new PropertyToSourceExtrasParser <string>(SMCommon.TagTickCounts, song.Extras), [SMCommon.TagInstrumentTrack] = new PropertyToSourceExtrasParser <string>(SMCommon.TagInstrumentTrack, song.Extras), [SMCommon.TagSampleStart] = new PropertyToSongPropertyParser(SMCommon.TagSampleStart, nameof(Song.PreviewSampleStart), song), [SMCommon.TagSampleLength] = new PropertyToSongPropertyParser(SMCommon.TagSampleLength, nameof(Song.PreviewSampleLength), song), [SMCommon.TagDisplayBPM] = new ListPropertyToSourceExtrasParser <string>(SMCommon.TagDisplayBPM, song.Extras), [SMCommon.TagSelectable] = new PropertyToSourceExtrasParser <string>(SMCommon.TagSelectable, song.Extras), [SMCommon.TagAnimations] = new PropertyToSourceExtrasParser <string>(SMCommon.TagAnimations, song.Extras), [SMCommon.TagBGChanges] = new PropertyToSourceExtrasParser <string>(SMCommon.TagBGChanges, song.Extras), [SMCommon.TagBGChanges1] = new PropertyToSourceExtrasParser <string>(SMCommon.TagBGChanges1, song.Extras), [SMCommon.TagBGChanges2] = new PropertyToSourceExtrasParser <string>(SMCommon.TagBGChanges2, song.Extras), [SMCommon.TagFGChanges] = new PropertyToSourceExtrasParser <string>(SMCommon.TagFGChanges, song.Extras), // TODO: Parse Keysounds properly. [SMCommon.TagKeySounds] = new PropertyToSourceExtrasParser <string>(SMCommon.TagKeySounds, song.Extras), [SMCommon.TagAttacks] = new ListPropertyToSourceExtrasParser <string>(SMCommon.TagAttacks, song.Extras), [SMCommon.TagNotes] = new SongNotesPropertyParser(SMCommon.TagNotes, song), [SMCommon.TagNotes2] = new SongNotesPropertyParser(SMCommon.TagNotes2, song), [SMCommon.TagLastBeatHint] = new PropertyToSourceExtrasParser <string>(SMCommon.TagLastBeatHint, song.Extras), }; foreach (var kvp in propertyParsers) { kvp.Value.SetLogger(Logger); } // Parse all Values from the MSDFile. foreach (var value in msdFile.Values) { if (propertyParsers.TryGetValue(value.Params[0]?.ToUpper() ?? "", out var propertyParser)) { propertyParser.Parse(value); } } // Insert stop and tempo change events. foreach (var chart in song.Charts) { SMCommon.AddStops(stops, chart); SMCommon.AddTempos(tempos, chart); } // Sort events. foreach (var chart in song.Charts) { chart.Layers[0].Events.Sort(new SMCommon.SMEventComparer()); } song.GenreTransliteration = song.Genre; var chartOffset = 0.0; if (song.Extras.TryGetSourceExtra(SMCommon.TagOffset, out object offsetObj)) { chartOffset = (double)offsetObj; } var chartMusicFile = ""; if (song.Extras.TryGetSourceExtra(SMCommon.TagMusic, out object chartMusicFileObj)) { chartMusicFile = (string)chartMusicFileObj; } var chartAuthor = ""; if (song.Extras.TryGetSourceExtra(SMCommon.TagCredit, out object chartAuthorObj)) { chartAuthor = (string)chartAuthorObj; } var chartDisplayTempo = SMCommon.GetDisplayBPMStringFromSourceExtrasList(song.Extras, tempos); foreach (var chart in song.Charts) { chart.MusicFile = chartMusicFile; chart.ChartOffsetFromMusic = chartOffset; chart.Tempo = chartDisplayTempo; chart.Artist = song.Artist; chart.ArtistTransliteration = song.ArtistTransliteration; chart.Genre = song.Genre; chart.GenreTransliteration = song.GenreTransliteration; chart.Author = chartAuthor; SMCommon.SetEventTimeMicros(chart); } return(song); }
private void FinalizeChartAndAddToSong( Chart chart, Dictionary <double, double> chartTempos, Dictionary <double, double> chartStops, Song song, Dictionary <double, double> songTempos, Dictionary <double, double> songStops) { // Do not add this Chart if we failed to parse the type. if (string.IsNullOrEmpty(chart.Type)) { return; } var useChartTimingData = false; if (chart.Extras.TryGetSourceExtra(SMCommon.TagFumenChartUsesOwnTimingData, out object useTimingDataObject)) { if (useTimingDataObject is bool b) { useChartTimingData = b; } } // Insert stop and tempo change events. SMCommon.AddStops(useChartTimingData ? chartStops : songStops, chart); SMCommon.AddTempos(useChartTimingData ? chartTempos : songTempos, chart); // Sort events. chart.Layers[0].Events.Sort(new SMCommon.SMEventComparer()); // Copy Song information over missing Chart information. if (string.IsNullOrEmpty(chart.MusicFile)) { var chartMusicFile = ""; if (song.Extras.TryGetSourceExtra(SMCommon.TagMusic, out object chartMusicFileObj)) { chartMusicFile = (string)chartMusicFileObj; } if (!string.IsNullOrEmpty(chartMusicFile)) { chart.MusicFile = chartMusicFile; } } if (string.IsNullOrEmpty(chart.Author)) { var chartAuthor = ""; if (song.Extras.TryGetSourceExtra(SMCommon.TagCredit, out object chartAuthorObj)) { chartAuthor = (string)chartAuthorObj; } if (!string.IsNullOrEmpty(chartAuthor)) { chart.Author = chartAuthor; } } if (string.IsNullOrEmpty(chart.Artist) && !string.IsNullOrEmpty(song.Artist)) { chart.Artist = song.Artist; } if (string.IsNullOrEmpty(chart.ArtistTransliteration) && !string.IsNullOrEmpty(song.ArtistTransliteration)) { chart.ArtistTransliteration = song.ArtistTransliteration; } if (string.IsNullOrEmpty(chart.Genre) && !string.IsNullOrEmpty(song.Genre)) { chart.Genre = song.Genre; } if (string.IsNullOrEmpty(chart.GenreTransliteration) && !string.IsNullOrEmpty(song.GenreTransliteration)) { chart.GenreTransliteration = song.GenreTransliteration; } if (!chart.Extras.TryGetSourceExtra(SMCommon.TagOffset, out object _) && song.Extras.TryGetSourceExtra(SMCommon.TagOffset, out object offsetObj)) { chart.ChartOffsetFromMusic = (double)offsetObj; } if (!chart.Extras.TryGetSourceExtra(SMCommon.TagDisplayBPM, out object _)) { chart.Tempo = SMCommon.GetDisplayBPMStringFromSourceExtrasList( song.Extras, useChartTimingData ? chartTempos : songTempos); } SMCommon.SetEventTimeMicros(chart); // Add the Chart. song.Charts.Add(chart); }
private void WriteMeasure(Chart chart, MeasureData measureData, int measureIndex) { // For UseLeastCommonMultiple and UseLeastCommonMultipleFromStepmaniaEditor we will // determine the number of lines to write per beat, assuming 4 beats per measure since // Stepmania enforces 4/4. // For UseSubDivisionDenominatorAsMeasureSpacing we will use the SubDivision denominator // as the measure spacing. // This helps maintain spacing for charts which were not written in Stepmania and // use technically unsupported spacing (like 14 notes per measure). var linesPerBeat = 1; var measureCharsDY = 0; switch (Config.MeasureSpacingBehavior) { case MeasureSpacingBehavior.UseSubDivisionDenominatorAsMeasureSpacing: { if (measureData.Notes.Count > 0) { foreach (var measureNote in measureData.Notes) { // Every note must also have the same denominator (number of lines in the measure). if (measureCharsDY == 0) { measureCharsDY = measureNote.Position.SubDivision.Denominator; } else if (measureCharsDY != measureNote.Position.SubDivision.Denominator) { Logger.Error($"Notes in measure {measureIndex} have inconsistent SubDivision denominators." + " These must all be equal when using UseSubDivisionDenominatorAsMeasureSpacing MeasureSpacingBehavior."); return; } } } // Still treat blank measures as 4 lines. else { measureCharsDY = SMCommon.NumBeatsPerMeasure; } break; } case MeasureSpacingBehavior.UseLeastCommonMultiple: { linesPerBeat = measureData.LCM; measureCharsDY = SMCommon.NumBeatsPerMeasure * linesPerBeat; break; } case MeasureSpacingBehavior.UseLeastCommonMultipleFromStepmaniaEditor: { // Make sure the notes can actually be represented by the stepmania editor. if (!SMCommon.GetLowestValidSMSubDivision(measureData.LCM, out linesPerBeat)) { Logger.Error($"Unsupported subdivisions {measureData.LCM} for notes in measure index {measureIndex}." + " Consider using UseLeastCommonMultiple MeasureSpacingBehavior."); return; } measureCharsDY = SMCommon.NumBeatsPerMeasure * linesPerBeat; break; } } // Set up a grid of characters to write. // TODO: Support keysound tagging. var measureCharsDX = chart.NumInputs; var measureChars = new char[measureCharsDX, measureCharsDY]; // Populate characters in the grid based on the events of the measure. foreach (var measureNote in measureData.Notes) { // Get the note char to write. var c = GetSMCharForNote(measureNote); // Determine the position to record the note. var reducedSubDivision = measureNote.Position.SubDivision.Reduce(); int measureEventPositionInMeasure; // When using UseSubDivisionDenominatorAsMeasureSpacing, get the y position directly from the extra data. if (Config.MeasureSpacingBehavior == MeasureSpacingBehavior.UseSubDivisionDenominatorAsMeasureSpacing) { var n = measureNote.Position.Beat * measureCharsDY + measureNote.Position.SubDivision.Numerator; if (n % SMCommon.NumBeatsPerMeasure != 0) { Logger.Error($"Note at position {measureNote.Position} has a position which cannot be mapped to {measureCharsDY} lines." + " When using UseSubDivisionDenominatorAsMeasureSpacing MeasureSpacingBehavior the notes in one measure must" + " all share the same SubDivision denominator and that denominator must be the number of lines for the measure."); return; } measureEventPositionInMeasure = n / SMCommon.NumBeatsPerMeasure; } // Otherwise calculate the position based on the lines per beat. else { measureEventPositionInMeasure = measureNote.Position.Beat * linesPerBeat + (linesPerBeat / Math.Max(1, reducedSubDivision.Denominator)) * reducedSubDivision.Numerator; } // Bounds checks. if (measureNote.Lane < 0 || measureNote.Lane >= chart.NumInputs) { Logger.Error($"Note at position {measureNote.Position} has invalid lane {measureNote.Lane}."); return; } if (measureEventPositionInMeasure < 0 || measureEventPositionInMeasure >= measureCharsDY) { Logger.Error($"Note has invalid position {measureNote.Position}."); return; } // Record the note. measureChars[measureNote.Lane, measureEventPositionInMeasure] = c; } // Write the measure of accumulated characters. if (!measureData.FirstMeasure) { StreamWriter.WriteLine(","); } for (var y = 0; y < measureCharsDY; y++) { for (var x = 0; x < measureCharsDX; x++) { StreamWriter.Write(measureChars[x, y] == '\0' ? SMCommon.NoteChars[(int)SMCommon.NoteType.None] : measureChars[x, y]); } StreamWriter.WriteLine(); } }
public override bool Parse(MSDFile.Value value) { // Only consider this line if it matches this property name. if (!DoesValueMatchProperty(value)) { return(false); } if (value.Params.Count < 7) { Logger?.Warn($"{PropertyName}: Expected at least 7 parameters. Found {value.Params.Count}. Ignoring all note data."); return(true); } var chart = new Chart { Type = value.Params[1]?.Trim(SMCommon.SMAllWhiteSpace) ?? "", Description = value.Params[2]?.Trim(SMCommon.SMAllWhiteSpace) ?? "", DifficultyType = value.Params[3]?.Trim(SMCommon.SMAllWhiteSpace) ?? "" }; chart.Layers.Add(new Layer()); // Record whether this chart was written under NOTES or NOTES2. chart.Extras.AddSourceExtra(SMCommon.TagFumenNotesType, PropertyName, true); // Parse the chart information before measure data. var chartDifficultyRatingStr = value.Params[4]?.Trim(SMCommon.SMAllWhiteSpace) ?? ""; var chartRadarValuesStr = value.Params[5]?.Trim(SMCommon.SMAllWhiteSpace) ?? ""; // Parse the difficulty rating as a number. if (int.TryParse(chartDifficultyRatingStr, out var difficultyRatingInt)) { chart.DifficultyRating = (double)difficultyRatingInt; } // Parse the radar values into a list. var radarValues = new List <double>(); var radarValuesStr = chartRadarValuesStr.Split(','); foreach (var radarValueStr in radarValuesStr) { if (double.TryParse(radarValueStr, out var d)) { radarValues.Add(d); } } chart.Extras.AddSourceExtra(SMCommon.TagRadarValues, radarValues, true); // Parse chart type and set number of players and inputs. if (!SMCommon.TryGetChartType(chart.Type, out var smChartType)) { Logger?.Error($"{PropertyName}: Failed to parse {SMCommon.TagStepsType} value '{chart.Type}'. This chart will be ignored."); return(true); } chart.NumPlayers = SMCommon.Properties[(int)smChartType].NumPlayers; chart.NumInputs = SMCommon.Properties[(int)smChartType].NumInputs; // Add a 4/4 time signature chart.Layers[0].Events.Add(new TimeSignature(new Fraction(SMCommon.NumBeatsPerMeasure, SMCommon.NumBeatsPerMeasure)) { Position = new MetricPosition() }); // Parse the notes. if (!ParseNotes(chart, value.Params[6] ?? "")) { return(true); } Song.Charts.Add(chart); return(true); }