示例#1
0
        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);
        }
示例#2
0
        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);
        }
示例#3
0
        /// <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);
        }
示例#4
0
        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);
        }
示例#5
0
        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();
            }
        }
示例#6
0
        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);
        }