Esempio n. 1
0
        /// <summary>
        /// For a mine positioned at the given arrow, determines how far away the neighboring
        /// arrow in the same lane is relative to arrows in other lanes. This is a number from
        /// [0, numArrows). Arrows sharing the same position (jumps and brackets) are considered
        /// at the same relative location.
        /// Example:
        /// X--- (This arrow is the 1st most recent relative to the mine.)
        /// -XX- (Both arrows here are the 0th most recent relative to the mine.)
        /// O--X (Mine. Arrow at this position is disqualified.)
        /// </summary>
        /// <param name="searchBackwards">
        /// If true then search backwards through the events.
        /// If false then search forwards through the events.
        /// </param>
        /// <param name="searchIndex">Search index into events to start the searching at.</param>
        /// <param name="numArrows">Number of arrows in the chart.</param>
        /// <param name="events">The List of FootActionEvent to search.</param>
        /// <param name="arrow">Arrow of the mine. Between [0, numArrows).</param>
        /// <returns>
        /// A tuple where the first value is the relative position of the neighboring arrow and
        /// the second value is the foot that was used for this arrow. If no arrow could be found
        /// then (InvalidArrowIndex, InvalidFoot) is returned.
        /// </returns>
        private static (int, int) GetHowRecentIsNeighboringArrow(
            bool searchBackwards,
            int searchIndex,
            int numArrows,
            List <FootActionEvent> events,
            int arrow)
        {
            var            currentN         = 0;
            var            consideredArrows = new bool[numArrows];
            MetricPosition currentNPosition = null;

            while (searchBackwards ? searchIndex >= 0 : searchIndex < events.Count)
            {
                if (!consideredArrows[events[searchIndex].Arrow])
                {
                    var newN = !(currentNPosition == null || currentNPosition == events[searchIndex].Position);
                    if (newN)
                    {
                        currentN++;
                    }
                    currentNPosition = events[searchIndex].Position;

                    if (events[searchIndex].Arrow == arrow)
                    {
                        return(currentN, events[searchIndex].Foot);
Esempio n. 2
0
        private int GetFootForArrow(int arrow, MetricPosition position, PerformedChart.PerformanceNode node)
        {
            while (node != null && node.Position == position)
            {
                if (node is PerformedChart.StepPerformanceNode spn)
                {
                    var previousStepLink = spn.GraphLinkInstance;
                    if (previousStepLink != null && !previousStepLink.GraphLink.IsRelease())
                    {
                        // If this step is a footswap we need to ignore the other foot, which may still be resting on this arrow.
                        var footSwap = previousStepLink.GraphLink.IsFootSwap(out var footSwapFoot, out var footSwapPortion);
                        if (footSwap && spn.GraphNodeInstance.Node.State[footSwapFoot, footSwapPortion].Arrow == arrow)
                        {
                            return(footSwapFoot);
                        }

                        // No footswap on the given arrow.
                        for (var f = 0; f < NumFeet; f++)
                        {
                            for (var p = 0; p < NumFootPortions; p++)
                            {
                                if (spn.GraphNodeInstance.Node.State[f, p].Arrow == arrow)
                                {
                                    return(f);
                                }
                            }
                        }
                    }
                }

                node = node.Next;
            }

            return(InvalidFoot);
        }
Esempio n. 3
0
        private int GetFootForArrow(int arrow, MetricPosition position, ExpressedChart.ChartSearchNode node)
        {
            while (node != null && node.Position == position)
            {
                if (node.PreviousLink != null && !node.PreviousLink.GraphLink.IsRelease())
                {
                    // If this step is a footswap we need to ignore the other foot, which may still be resting on this arrow.
                    var previousStepLink = node.GetPreviousStepLink();
                    var footSwapFoot     = InvalidFoot;
                    var footSwapPortion  = DefaultFootPortion;
                    var footSwap         = previousStepLink?.GraphLink.IsFootSwap(out footSwapFoot, out footSwapPortion) ?? false;
                    if (footSwap && node.GraphNode.State[footSwapFoot, footSwapPortion].Arrow == arrow)
                    {
                        return(footSwapFoot);
                    }

                    // No footswap on the given arrow.
                    for (var f = 0; f < NumFeet; f++)
                    {
                        for (var p = 0; p < NumFootPortions; p++)
                        {
                            if (node.GraphNode.State[f, p].Arrow == arrow)
                            {
                                return(f);
                            }
                        }
                    }
                }

                node = node.GetNextNode();
            }

            return(InvalidFoot);
        }
Esempio n. 4
0
        private void WriteMine(int arrow, int firstLaneX, double y, MetricPosition position)
        {
            var x = firstLaneX + (arrow * ArrowW);

            StreamWriter.Write(
                $@"			<img class=""mine"" style=""top:{(int)(y - ArrowW * 0.5)}px; left:{x}px; z-index:{(int)y};""/>
");
        }
Esempio n. 5
0
        /// <summary>
        /// Sets the TimeMicros on the given Chart Events based on the
        /// Tempo, TimeSignatures, and MetricPositions of Events in the Chart.
        /// </summary>
        /// <param name="chart">Chart to set TimeMicros on the Events.</param>
        public static void SetEventTimeMicros(Chart chart)
        {
            var    bpm           = 0.0;
            var    timeSignature = new Fraction(4, 4);
            double beatTime      = 0.0;
            double currentTime   = 0.0;

            var previousPosition = new MetricPosition();

            foreach (var chartEvent in chart.Layers[0].Events)
            {
                if (chartEvent.Position > previousPosition)
                {
                    var currentBeats =
                        chartEvent.Position.Measure * timeSignature.Numerator
                        + chartEvent.Position.Beat
                        + (chartEvent.Position.SubDivision.Denominator == 0 ? 0 : chartEvent.Position.SubDivision.ToDouble());
                    var previousBeats =
                        previousPosition.Measure * timeSignature.Numerator
                        + previousPosition.Beat
                        + (previousPosition.SubDivision.Denominator == 0 ? 0 : previousPosition.SubDivision.ToDouble());
                    currentTime += (currentBeats - previousBeats) * beatTime;
                }

                chartEvent.TimeMicros = (long)(currentTime * 1000000);

                var beatTimeDirty = false;
                if (chartEvent is Stop stop)
                {
                    currentTime += (double)stop.LengthMicros / 1000000;
                }
                else if (chartEvent is TimeSignature ts)
                {
                    timeSignature = ts.Signature;
                    beatTimeDirty = true;
                }
                else if (chartEvent is TempoChange tc)
                {
                    bpm           = tc.TempoBPM;
                    beatTimeDirty = true;
                }

                if (beatTimeDirty)
                {
                    if (bpm == 0.0 || timeSignature.Denominator == 0.0)
                    {
                        beatTime = 0.0;
                    }
                    else
                    {
                        beatTime = (60 / bpm) * (4.0 / timeSignature.Denominator);
                    }
                }

                previousPosition = chartEvent.Position;
            }
        }
Esempio n. 6
0
        /// <summary>
        /// Helper to encapsulate the logic around swapping sides and recording data into the right
        /// variables.
        /// </summary>
        private static void SwapSides(
            ref int currentStepsBetweenSideGreatestDenominator,
            MetricPosition currentPosition,
            MetricPosition previousPosition,
            ref bool currentStepsBetweenSideUseVariableTiming,
            List <Tuple <double, int> > stepsBetweenSideChangesVariableSpacing,
            Dictionary <int, List <Tuple <double, int> > > stepsBetweenSideChanges,
            double currentTime,
            ref double timeOfFirstStepOnCurrentSide,
            ref int currentStepCountOnSide)
        {
            // If the notes are spaced greater than expected (e.g. eighth note up beats, or half/whole notes) then
            // treat as variable timing.
            if (currentStepsBetweenSideGreatestDenominator > 0)
            {
                // Assumption that time signature is 4/4.
                var expectedBeatsForConsistentTiming = 1.0 / currentStepsBetweenSideGreatestDenominator;
                var currentBeats =
                    currentPosition.Measure * 4
                    + currentPosition.Beat
                    + (currentPosition.SubDivision.Denominator == 0 ? 0 : currentPosition.SubDivision.ToDouble());
                var previousBeats =
                    previousPosition.Measure * 4
                    + previousPosition.Beat
                    + (previousPosition.SubDivision.Denominator == 0 ? 0 : previousPosition.SubDivision.ToDouble());

                // Notes are spaced too far apart to be a proper stream.
                if (Math.Abs(currentBeats - (previousBeats + expectedBeatsForConsistentTiming)) > 0.001)
                {
                    currentStepsBetweenSideUseVariableTiming = true;
                }
            }

            var time  = currentTime - timeOfFirstStepOnCurrentSide;
            var entry = new Tuple <double, int>(time, currentStepCountOnSide);

            if (currentStepsBetweenSideUseVariableTiming)
            {
                stepsBetweenSideChangesVariableSpacing.Add(entry);
            }
            else if (stepsBetweenSideChanges.ContainsKey(currentStepsBetweenSideGreatestDenominator))
            {
                stepsBetweenSideChanges[currentStepsBetweenSideGreatestDenominator].Add(entry);
            }
            // If we recorded a time signature, but it isn't a valid subdivision, just use the highest subdivision.
            else
            {
                stepsBetweenSideChanges[48].Add(entry);
            }

            // Reset state.
            currentStepCountOnSide                     = 0;
            timeOfFirstStepOnCurrentSide               = 0.0;
            currentStepsBetweenSideUseVariableTiming   = false;
            currentStepsBetweenSideGreatestDenominator = 0;
        }
Esempio n. 7
0
 protected Event(Event other)
 {
     TimeMicros = other.TimeMicros;
     if (other.Position != null)
     {
         Position = new MetricPosition(other.Position);
     }
     SourceType = other.SourceType;
     DestType   = other.DestType;
     Extras     = new Extras(other.Extras);
 }
Esempio n. 8
0
        private void WriteArrow(int arrow, int foot, int firstLaneX, double y, MetricPosition position)
        {
            var rotClass = ArrowClassStrings[arrow % 4];
            var x        = firstLaneX + arrow * ArrowW;
            var fraction = position.SubDivision.Reduce();

            string imgClass;

            switch (fraction.Denominator)
            {
            case 0:
            case 1: imgClass = "quarter"; break;

            case 2: imgClass = "eighth"; break;

            case 3: imgClass = "twelfth"; break;

            case 4: imgClass = "sixteenth"; break;

            case 6: imgClass = "twentyfourth"; break;

            case 8: imgClass = "thirtysecond"; break;

            case 12: imgClass = "fourtyeighth"; break;

            default: imgClass = "sixtyfourth"; break;
            }

            string classStr;

            if (rotClass != null)
            {
                classStr = $"{imgClass} {rotClass}";
            }
            else
            {
                classStr = $"{imgClass}";
            }

            // Arrow
            StreamWriter.Write(
                $@"			<img class=""{classStr}"" style=""top:{(int)(y - ArrowW * 0.5)}px; left:{x}px; z-index:{(int)y};""/>
");
            // Foot indicator
            if (foot != InvalidFoot)
            {
                var footClass = foot == L ? "leftfoot" : "rightfoot";
                StreamWriter.Write(
                    $@"			<img class=""{footClass}"" style=""top:{(int)(y - ArrowW * 0.5)}px; left:{x}px; z-index:{(int)y};""/>
");
            }
        }
Esempio n. 9
0
        /// <summary>
        /// Adds charts to the given song and write a visualization per chart, if configured to do so.
        /// </summary>
        /// <param name="song">Song to add charts to.</param>
        /// <param name="songArgs">SongArgs for the song file.</param>
        private static void AddCharts(Song song, SongArgs songArgs)
        {
            LogInfo("Processing Song.", songArgs.FileInfo, songArgs.RelativePath, song);

            var fileNameNoExtension = songArgs.FileInfo.Name;

            if (!string.IsNullOrEmpty(songArgs.FileInfo.Extension))
            {
                fileNameNoExtension =
                    fileNameNoExtension.Substring(0, songArgs.FileInfo.Name.Length - songArgs.FileInfo.Extension.Length);
            }

            var extension = songArgs.FileInfo.Extension.ToLower();

            if (extension.StartsWith("."))
            {
                extension = extension.Substring(1);
            }

            var newCharts             = new List <Chart>();
            var chartsIndicesToRemove = new List <int>();

            foreach (var chart in song.Charts)
            {
                if (chart.Layers.Count == 1 &&
                    chart.Type == Config.Instance.InputChartType &&
                    chart.NumPlayers == 1 &&
                    chart.NumInputs == InputStepGraph.NumArrows &&
                    Config.Instance.DifficultyMatches(chart.DifficultyType))
                {
                    // Check if there is an existing chart.
                    var(currentChart, currentChartIndex) = FindChart(
                        song,
                        Config.Instance.OutputChartType,
                        chart.DifficultyType,
                        OutputStepGraph.NumArrows);
                    if (currentChart != null)
                    {
                        var fumenGenerated = GetFumenGeneratedVersion(currentChart, out var version);

                        // Check if we should skip or overwrite the chart.
                        switch (Config.Instance.OverwriteBehavior)
                        {
                        case OverwriteBehavior.DoNotOverwrite:
                            continue;

                        case OverwriteBehavior.IfFumenGenerated:
                            if (!fumenGenerated)
                            {
                                continue;
                            }
                            break;

                        case OverwriteBehavior.IfFumenGeneratedAndNewerVersion:
                            if (!fumenGenerated || version >= Version)
                            {
                                continue;
                            }
                            break;

                        case OverwriteBehavior.Always:
                        default:
                            break;
                        }
                    }

                    // Create an ExpressedChart.
                    var(ecc, eccName) = Config.Instance.GetExpressedChartConfig(songArgs.FileInfo, chart.DifficultyType);
                    var expressedChart = ExpressedChart.CreateFromSMEvents(
                        chart.Layers[0].Events,
                        InputStepGraph,
                        ecc,
                        chart.DifficultyRating,
                        GetLogIdentifier(songArgs.FileInfo, songArgs.RelativePath, song, chart));
                    if (expressedChart == null)
                    {
                        LogError("Failed to create ExpressedChart.", songArgs.FileInfo, songArgs.RelativePath, song, chart);
                        continue;
                    }

                    // Create a PerformedChart.
                    var(pcc, pccName) = Config.Instance.GetPerformedChartConfig(songArgs.FileInfo, chart.DifficultyType);
                    var performedChart = PerformedChart.CreateFromExpressedChart(
                        OutputStepGraph,
                        pcc,
                        OutputStartNodes,
                        expressedChart,
                        GeneratePerformedChartRandomSeed(songArgs.FileInfo.Name),
                        GetLogIdentifier(songArgs.FileInfo, songArgs.RelativePath, song, chart));
                    if (performedChart == null)
                    {
                        LogError("Failed to create PerformedChart.", songArgs.FileInfo, songArgs.RelativePath, song, chart);
                        continue;
                    }

                    // At this point we have succeeded, so add the chart index to remove if appropriate.
                    if (currentChart != null)
                    {
                        chartsIndicesToRemove.Add(currentChartIndex);
                    }

                    // Create Events for the new Chart.
                    var events = performedChart.CreateSMChartEvents();
                    CopyNonPerformanceEvents(chart.Layers[0].Events, events);
                    events.Sort(new SMEventComparer());

                    // Sanity check on note counts.
                    if (events.Count != chart.Layers[0].Events.Count)
                    {
                        var mineString = NoteChars[(int)NoteType.Mine].ToString();
                        // Disregard discrepancies in mine counts
                        var newChartNonMineEventCount = 0;
                        foreach (var newEvent in events)
                        {
                            if (newEvent.SourceType != mineString)
                            {
                                newChartNonMineEventCount++;
                            }
                        }

                        var oldChartNonMineEventCount = 0;
                        foreach (var oldEvent in chart.Layers[0].Events)
                        {
                            if (oldEvent.SourceType != mineString)
                            {
                                oldChartNonMineEventCount++;
                            }
                        }

                        if (newChartNonMineEventCount != oldChartNonMineEventCount)
                        {
                            MetricPosition firstDiscrepancyPosition = null;
                            var            i = 0;
                            while (i < events.Count && i < chart.Layers[0].Events.Count)
                            {
                                if (events[i].SourceType != chart.Layers[0].Events[i].SourceType ||
                                    events[i].Position != chart.Layers[0].Events[i].Position)
                                {
                                    firstDiscrepancyPosition = chart.Layers[0].Events[i].Position;
                                    break;
                                }

                                i++;
                            }

                            LogError(
                                "Programmer error. Discrepancy in non-mine Event counts."
                                + $" Old: {oldChartNonMineEventCount}, New: {newChartNonMineEventCount}."
                                + $" First discrepancy position: {firstDiscrepancyPosition}.",
                                songArgs.FileInfo, songArgs.RelativePath, song, chart);
                            continue;
                        }
                    }

                    // Create a new Chart for these Events.
                    var newChart = new Chart
                    {
                        Artist = chart.Artist,
                        ArtistTransliteration = chart.ArtistTransliteration,
                        Genre = chart.Genre,
                        GenreTransliteration = chart.GenreTransliteration,
                        Author               = FormatWithVersion(chart.Author),
                        Description          = FormatWithVersion(chart.Description),
                        MusicFile            = chart.MusicFile,
                        ChartOffsetFromMusic = chart.ChartOffsetFromMusic,
                        Tempo            = chart.Tempo,
                        DifficultyRating = chart.DifficultyRating,
                        DifficultyType   = chart.DifficultyType,
                        Extras           = chart.Extras,
                        Type             = Config.Instance.OutputChartType,
                        NumPlayers       = 1,
                        NumInputs        = OutputStepGraph.NumArrows
                    };
                    newChart.Layers.Add(new Layer {
                        Events = events
                    });
                    newCharts.Add(newChart);

                    LogInfo(
                        $"Generated new {newChart.Type} {newChart.DifficultyType} Chart from {chart.Type} {chart.DifficultyType} Chart"
                        + $" using ExpressedChartConfig \"{eccName}\" (BracketParsingMethod {expressedChart.GetBracketParsingMethod():G})"
                        + $" and PerformedChartConfig \"{pccName}\".",
                        songArgs.FileInfo, songArgs.RelativePath, song, newChart);

                    // Write a visualization.
                    if (Config.Instance.OutputVisualizations && CanOutputVisualizations)
                    {
                        var visualizationDirectory = Fumen.Path.Combine(VisualizationDir, songArgs.RelativePath);
                        Directory.CreateDirectory(visualizationDirectory);
                        var saveFile = Fumen.Path.GetWin32FileSystemFullPath(
                            Fumen.Path.Combine(visualizationDirectory,
                                               $"{fileNameNoExtension}-{chart.DifficultyType}-{extension}.html"));

                        try
                        {
                            var visualizer = new Visualizer(
                                songArgs.CurrentDir,
                                saveFile,
                                song,
                                chart,
                                expressedChart,
                                eccName,
                                performedChart,
                                pccName,
                                newChart
                                );
                            visualizer.Write();
                        }
                        catch (Exception e)
                        {
                            LogError($"Failed to write visualization to \"{saveFile}\". {e}",
                                     songArgs.FileInfo, songArgs.RelativePath, song, newChart);
                        }
                    }
                }
            }

            LogInfo(
                $"Generated {newCharts.Count} new {Config.Instance.OutputChartType} Charts (replaced {chartsIndicesToRemove.Count}).",
                songArgs.FileInfo, songArgs.RelativePath, song);

            // Remove overwritten charts.
            if (chartsIndicesToRemove.Count > 0)
            {
                // Ensure the indices are sorted descending so they don't shift when removing.
                chartsIndicesToRemove.Sort((a, b) => b.CompareTo(a));
                foreach (var i in chartsIndicesToRemove)
                {
                    song.Charts.RemoveAt(i);
                }
            }

            // Add new charts.
            song.Charts.AddRange(newCharts);
        }
Esempio n. 10
0
        /// <summary>
        /// Process the song represented by the given FileInfo.
        /// Record stats for each chart into the appropriate CSV StringBuilders.
        /// </summary>
        /// <param name="fileInfo">FileInfo representing a song.</param>
        static async Task ProcessSong(FileInfo fileInfo)
        {
            Logger.Info($"Processing {fileInfo.Name}.");

            var reader = Reader.CreateReader(fileInfo);
            var song   = await reader.Load();

            foreach (var chart in song.Charts)
            {
                if (chart.Layers.Count == 1 &&
                    chart.Type == Config.Instance.InputChartType &&
                    chart.NumPlayers == 1 &&
                    Config.Instance.DifficultyMatches(chart.DifficultyType))
                {
                    // Variable to record steps per side change with variable spacing.
                    // This data will go in its own column in the csv for ease of graphing.
                    // Tuple first value is time for the series of steps.
                    // Tuple second value is hte number of steps in the series.
                    var stepsBetweenSideChangesVariableSpacing = new List <Tuple <double, int> >();

                    // Variable to record steps per side change with constant spacing (streams).
                    // The key in this dictionary is the denominator of the beat subdivision.
                    // This helps group by note type (quarter, eighth, etc.) for graphing.
                    // Each note type will go in its own column in the csv for ease of graphing.
                    // Tuple first value is time for the series of steps.
                    // Tuple second value is hte number of steps in the series.
                    var stepsBetweenSideChanges = new Dictionary <int, List <Tuple <double, int> > >();
                    foreach (var denom in SMCommon.ValidDenominators)
                    {
                        stepsBetweenSideChanges[denom] = new List <Tuple <double, int> >();
                    }

                    var totalSteps                                 = 0;
                    var steps                                      = new int[chart.NumInputs];
                    var onLeftSide                                 = false;
                    var onRightSide                                = false;
                    var currentStepCountOnSide                     = 0;
                    var previousStepWasFullyOnLeft                 = false;
                    var previousStepWasFullyOnRight                = false;
                    var currentHolds                               = new bool[chart.NumInputs];
                    var firstNote                                  = true;
                    var firstNoteTimeMicros                        = 0L;
                    var lastNoteTimeMicros                         = 0L;
                    var previousSteps                              = new bool[chart.NumInputs];
                    var currentSteps                               = new bool[chart.NumInputs];
                    var timeOfFirstStepOnCurrentSide               = 0.0;
                    var previousTimeBetweenSteps                   = 0.0;
                    var previousTime                               = 0.0;
                    var previousPosition                           = new MetricPosition();
                    var currentStepsBetweenSideUseVariableTiming   = false;
                    var currentStepsBetweenSideGreatestDenominator = 0;
                    var peakNPS                                    = 0.0;
                    var npsPerNote                                 = new List <double>();

                    // Parse each event in the chart. Loop index incremented in internal while loop
                    // to capture jumps.
                    for (var i = 0; i < chart.Layers[0].Events.Count;)
                    {
                        var            currentStepsOnLeft  = false;
                        var            currentStepsOnRight = false;
                        double         currentTime;
                        MetricPosition currentPosition;
                        var            currentTimeBetweenSteps = 0.0;
                        var            wasAStep = false;
                        for (var s = 0; s < chart.NumInputs; s++)
                        {
                            currentSteps[s] = false;
                        }
                        var numStepsAtThisPosition = 0;
                        var firstStep = totalSteps == 0;

                        // Process each note at the same position (capture jumps).
                        do
                        {
                            var chartEvent = chart.Layers[0].Events[i];
                            currentPosition = chartEvent.Position;
                            currentTime     = chartEvent.TimeMicros / 1000000.0;

                            // Record data about the step.
                            var lane = -1;
                            if (chartEvent is LaneHoldStartNote lhsn)
                            {
                                lane = lhsn.Lane;
                                currentHolds[lane] = true;
                                if (firstNote)
                                {
                                    firstNoteTimeMicros = lhsn.TimeMicros;
                                }
                                lastNoteTimeMicros = lhsn.TimeMicros;
                                firstNote          = false;
                                currentSteps[lane] = true;
                            }
                            else if (chartEvent is LaneHoldEndNote lhen)
                            {
                                currentHolds[lhen.Lane] = false;
                            }
                            else if (chartEvent is LaneTapNote ltn)
                            {
                                lane = ltn.Lane;
                                if (firstNote)
                                {
                                    firstNoteTimeMicros = ltn.TimeMicros;
                                }
                                lastNoteTimeMicros = ltn.TimeMicros;
                                firstNote          = false;
                                currentSteps[lane] = true;
                            }

                            // This note was a step on an arrow.
                            if (lane >= 0)
                            {
                                if (lane < chart.NumInputs >> 1)
                                {
                                    currentStepsOnLeft = true;
                                }
                                else
                                {
                                    currentStepsOnRight = true;
                                }

                                totalSteps++;
                                steps[lane]++;
                                if (currentStepCountOnSide == 0)
                                {
                                    timeOfFirstStepOnCurrentSide = currentTime;
                                }
                                currentStepCountOnSide++;
                                wasAStep = true;
                                numStepsAtThisPosition++;
                                currentStepsBetweenSideGreatestDenominator = Math.Max(
                                    currentStepsBetweenSideGreatestDenominator,
                                    chartEvent.Position.SubDivision.Reduce().Denominator);
                            }

                            i++;
                        }
                        // Continue looping if the next event is at the same position.
                        while (i < chart.Layers[0].Events.Count &&
                               chart.Layers[0].Events[i].Position == chart.Layers[0].Events[i - 1].Position);

                        if (wasAStep)
                        {
                            currentTimeBetweenSteps = currentTime - previousTime;
                            if (Math.Abs(currentTimeBetweenSteps - previousTimeBetweenSteps) > 0.001)
                            {
                                currentStepsBetweenSideUseVariableTiming = true;
                            }
                        }

                        // NPS tracking
                        if (numStepsAtThisPosition > 0 && !firstStep)
                        {
                            var stepNPS = 0.0;
                            if (currentTimeBetweenSteps > 0.0)
                            {
                                stepNPS = numStepsAtThisPosition / currentTimeBetweenSteps;
                            }
                            if (stepNPS > peakNPS)
                            {
                                peakNPS = stepNPS;
                            }

                            for (var npsStep = 0; npsStep < numStepsAtThisPosition; npsStep++)
                            {
                                npsPerNote.Add(stepNPS);
                            }

                            // Add entries for the first notes using the second notes' time.
                            if (npsPerNote.Count < totalSteps)
                            {
                                for (var npsStep = 0; npsStep < numStepsAtThisPosition; npsStep++)
                                {
                                    npsPerNote.Add(stepNPS);
                                }
                            }
                        }

                        // Quick and somewhat sloppy check to determine if this step is a jack.
                        var jack = true;
                        for (var s = 0; s < chart.NumInputs; s++)
                        {
                            if (currentSteps[s] && !previousSteps[s])
                            {
                                jack = false;
                            }
                        }

                        var currentStepIsFullyOnLeft  = currentStepsOnLeft && !currentStepsOnRight;
                        var currentStepIsFullyOnRight = currentStepsOnRight && !currentStepsOnLeft;

                        // Determine if any are held on each side so we don't consider steps on the other side
                        // to be the start of a new sequence.
                        var anyHeldOnLeft  = false;
                        var anyHeldOnRight = false;
                        for (var a = 0; a < chart.NumInputs; a++)
                        {
                            if (currentHolds[a])
                            {
                                if (a < chart.NumInputs >> 1)
                                {
                                    anyHeldOnLeft = true;
                                }
                                else
                                {
                                    anyHeldOnRight = true;
                                }
                            }
                        }

                        // Check for the steps representing the player being fully on the left.
                        if (currentStepIsFullyOnLeft && previousStepWasFullyOnLeft && !anyHeldOnRight && !jack)
                        {
                            // If we were on the right, swap sides.
                            if (onRightSide)
                            {
                                SwapSides(
                                    ref currentStepsBetweenSideGreatestDenominator,
                                    currentPosition,
                                    previousPosition,
                                    ref currentStepsBetweenSideUseVariableTiming,
                                    stepsBetweenSideChangesVariableSpacing,
                                    stepsBetweenSideChanges,
                                    currentTime,
                                    ref timeOfFirstStepOnCurrentSide,
                                    ref currentStepCountOnSide);
                            }
                            onLeftSide  = true;
                            onRightSide = false;
                        }

                        // Check for the steps representing the player being fully on the right.
                        if (currentStepIsFullyOnRight && previousStepWasFullyOnRight && !anyHeldOnLeft && !jack)
                        {
                            // If we were on the left, swap sides.
                            if (onLeftSide)
                            {
                                SwapSides(
                                    ref currentStepsBetweenSideGreatestDenominator,
                                    currentPosition,
                                    previousPosition,
                                    ref currentStepsBetweenSideUseVariableTiming,
                                    stepsBetweenSideChangesVariableSpacing,
                                    stepsBetweenSideChanges,
                                    currentTime,
                                    ref timeOfFirstStepOnCurrentSide,
                                    ref currentStepCountOnSide);
                            }
                            onRightSide = true;
                            onLeftSide  = false;
                        }

                        // Record data about the previous step for the next iteration if this was a step.
                        if (wasAStep)
                        {
                            previousStepWasFullyOnLeft  = currentStepIsFullyOnLeft;
                            previousStepWasFullyOnRight = currentStepIsFullyOnRight;
                            for (var s = 0; s < chart.NumInputs; s++)
                            {
                                previousSteps[s] = currentSteps[s];
                            }
                            previousTime             = currentTime;
                            previousTimeBetweenSteps = currentTimeBetweenSteps;
                            previousPosition         = currentPosition;
                        }
                    }

                    // Don't record anything if there are no steps. This prevents unhelpful data (and NaNs) from showing up.
                    if (totalSteps == 0)
                    {
                        continue;
                    }

                    var playTime = (lastNoteTimeMicros - firstNoteTimeMicros) / 1000000.0;
                    var nps      = totalSteps / playTime;

                    // Record song stats.
                    SBStats.Append($"{CSVEscape(fileInfo.Directory.FullName)},{CSVEscape(fileInfo.Name)},");
                    SBStats.Append(
                        $"{CSVEscape(song.Title)},{CSVEscape(chart.Type)},{CSVEscape(chart.DifficultyType)},{chart.DifficultyRating},");

                    // NPS
                    SBStats.Append($"{nps},");
                    SBStats.Append($"{peakNPS},");
                    var numUnderHalf = 0;
                    var numOver2x    = 0;
                    var numOver3x    = 0;
                    var numOver4x    = 0;
                    foreach (var noteNPS in npsPerNote)
                    {
                        if (noteNPS < 0.5 * nps)
                        {
                            numUnderHalf++;
                        }
                        else if (noteNPS > 2.0 * nps && noteNPS <= 3.0 * nps)
                        {
                            numOver2x++;
                        }
                        else if (noteNPS > 3.0 * nps && noteNPS <= 4.0 * nps)
                        {
                            numOver3x++;
                        }
                        else if (noteNPS > 4.0 * nps)
                        {
                            numOver4x++;
                        }
                    }
                    SBStats.Append($"{(double)numUnderHalf / totalSteps},");
                    SBStats.Append($"{(double)numOver2x / totalSteps},");
                    SBStats.Append($"{(double)numOver3x / totalSteps},");
                    SBStats.Append($"{(double)numOver4x / totalSteps},");

                    SBStats.Append($"{totalSteps},");
                    for (var i = 0; i < chart.NumInputs; i++)
                    {
                        SBStats.Append($"{steps[i]},");
                    }
                    for (var i = 0; i < chart.NumInputs; i++)
                    {
                        SBStats.Append($"{(double) steps[i] / totalSteps},");
                    }
                    SBStats.AppendLine("");

                    // Record data about the steps taken per each side of the pads.
                    foreach (var denominator in SMCommon.ValidDenominators)
                    {
                        foreach (var sideChange in stepsBetweenSideChanges[denominator])
                        {
                            SBStepsPerSide.Append($"{CSVEscape(fileInfo.Directory.FullName)},{CSVEscape(fileInfo.Name)},");
                            SBStepsPerSide.Append(
                                $"{CSVEscape(song.Title)},{CSVEscape(chart.Type)},{CSVEscape(chart.DifficultyType)},{chart.DifficultyRating},");
                            SBStepsPerSide.Append($"{sideChange.Item1}");

                            // Count pass
                            foreach (var lineDenominator in SMCommon.ValidDenominators)
                            {
                                if (lineDenominator == denominator)
                                {
                                    SBStepsPerSide.Append($",{sideChange.Item2}");
                                }
                                else
                                {
                                    SBStepsPerSide.Append(",");
                                }
                            }
                            SBStepsPerSide.Append(",");

                            // NPS pass
                            SBStepsPerSide.Append($",{sideChange.Item2 / sideChange.Item1}");
                            foreach (var lineDenominator in SMCommon.ValidDenominators)
                            {
                                if (lineDenominator == denominator)
                                {
                                    SBStepsPerSide.Append($",{sideChange.Item2 / sideChange.Item1}");
                                }
                                else
                                {
                                    SBStepsPerSide.Append(",");
                                }
                            }
                            SBStepsPerSide.AppendLine(",");
                        }
                    }
                    foreach (var sideChange in stepsBetweenSideChangesVariableSpacing)
                    {
                        SBStepsPerSide.Append($"{CSVEscape(fileInfo.Directory.FullName)},{CSVEscape(fileInfo.Name)},");
                        SBStepsPerSide.Append(
                            $"{CSVEscape(song.Title)},{CSVEscape(chart.Type)},{CSVEscape(chart.DifficultyType)},{chart.DifficultyRating},");
                        SBStepsPerSide.Append($"{sideChange.Item1}");

                        // Count pass
                        foreach (var _ in SMCommon.ValidDenominators)
                        {
                            SBStepsPerSide.Append(",");
                        }
                        SBStepsPerSide.Append($",{sideChange.Item2}");

                        // NPS pass
                        SBStepsPerSide.Append($",{sideChange.Item2 / sideChange.Item1}");
                        foreach (var _ in SMCommon.ValidDenominators)
                        {
                            SBStepsPerSide.Append(",");
                        }
                        SBStepsPerSide.AppendLine($",{sideChange.Item2 / sideChange.Item1}");
                    }
                }
            }
        }
Esempio n. 11
0
        private void WriteChart(Chart chart, int chartXPosition, bool originalChart)
        {
            for (var f = 0; f < NumFeet; f++)
            {
                LastExpressionPosition[f] = -1.0;
            }

            var firstLaneX = chartXPosition;

            foreach (var chartCol in ChartColumnInfo)
            {
                firstLaneX += chartCol.Width;
            }

            var previousTimeSignaturePosition = new MetricPosition();
            var previousTimeSignatureY        = ArrowW * 0.5;
            var currentTimeSignature          = new Fraction(4, 4);
            var yPerBeat = (double)BeatYSeparation;

            var lastHoldStarts  = new int[chart.NumInputs];
            var lastHoldWasRoll = new bool[chart.NumInputs];

            var currentExpressedChartSearchNode = ExpressedChart.GetRootSearchNode();
            var currentPerformedChartNode       = PerformedChart.GetRootPerformanceNode();
            var currentExpressedMineIndex       = 0;

            foreach (var chartEvent in chart.Layers[0].Events)
            {
                double eventY =
                    previousTimeSignatureY
                    + (chartEvent.Position.Measure - previousTimeSignaturePosition.Measure) * currentTimeSignature.Numerator * yPerBeat
                    + chartEvent.Position.Beat * yPerBeat
                    + (chartEvent.Position.SubDivision.Denominator == 0
                                                ? 0
                                                : chartEvent.Position.SubDivision.ToDouble() * yPerBeat);

                while (currentExpressedChartSearchNode != null && currentExpressedChartSearchNode.Position < chartEvent.Position)
                {
                    currentExpressedChartSearchNode = currentExpressedChartSearchNode.GetNextNode();
                }
                while (currentPerformedChartNode != null && currentPerformedChartNode.Position < chartEvent.Position)
                {
                    currentPerformedChartNode = currentPerformedChartNode.Next;
                }
                while (currentExpressedMineIndex < ExpressedChart.MineEvents.Count &&
                       ExpressedChart.MineEvents[currentExpressedMineIndex].Position < chartEvent.Position)
                {
                    currentExpressedMineIndex++;
                }

                if (chartEvent is TimeSignature ts)
                {
                    // Write measure markers up until this time signature change
                    WriteMeasures(
                        chartXPosition,
                        previousTimeSignatureY,
                        yPerBeat,
                        currentTimeSignature,
                        previousTimeSignaturePosition.Measure,
                        chartEvent.Position.Measure - previousTimeSignaturePosition.Measure,
                        chart.NumInputs);

                    // Update time signature tracking
                    previousTimeSignatureY = eventY;
                    currentTimeSignature   = ts.Signature;
                    yPerBeat = BeatYSeparation * (4.0 / currentTimeSignature.Denominator);
                    previousTimeSignaturePosition = chartEvent.Position;
                }

                // Chart column values. Excluding measures which are handled in WriteMeasures.
                var    colX   = chartXPosition;
                var    colW   = ChartColumnInfo[(int)ChartColumns.TimeSignature].Width;
                var    colY   = (int)(eventY - ChartTextH * .5);
                string colVal = null;
                if (chartEvent is TimeSignature tse)
                {
                    colVal = $"{tse.Signature.Numerator}/{tse.Signature.Denominator}";
                    colX  += ChartColumnInfo[(int)ChartColumns.TimeSignature].X;
                }
                else if (chartEvent is Stop stop)
                {
                    colVal = $"{stop.LengthMicros / 1000000.0}";
                    colX  += ChartColumnInfo[(int)ChartColumns.Stop].X;
                }
                else if (chartEvent is TempoChange tc)
                {
                    colVal = $"{tc.TempoBPM}";
                    colX  += ChartColumnInfo[(int)ChartColumns.BPM].X;
                }

                if (colVal != null)
                {
                    StreamWriter.Write(
                        $@"			<p class=""exp_text"" style=""top:{colY}px; left:{colX}px; width:{colW}px;"">{colVal}</p>
");
                }

                // Arrows
                if (chartEvent is LaneTapNote ltn)
                {
                    if (originalChart)
                    {
                        WriteExpression(currentExpressedChartSearchNode, eventY);
                    }

                    var foot = InvalidFoot;
                    if (originalChart)
                    {
                        foot = GetFootForArrow(ltn.Lane, ltn.Position, currentExpressedChartSearchNode);
                    }
                    else
                    {
                        foot = GetFootForArrow(ltn.Lane, ltn.Position, currentPerformedChartNode);
                    }
                    WriteArrow(ltn.Lane, foot, firstLaneX, eventY, ltn.Position);
                }
                else if (chartEvent is LaneHoldStartNote lhsn)
                {
                    if (originalChart)
                    {
                        WriteExpression(currentExpressedChartSearchNode, eventY);
                    }

                    var foot = InvalidFoot;
                    if (originalChart)
                    {
                        foot = GetFootForArrow(lhsn.Lane, lhsn.Position, currentExpressedChartSearchNode);
                    }
                    else
                    {
                        foot = GetFootForArrow(lhsn.Lane, lhsn.Position, currentPerformedChartNode);
                    }
                    WriteArrow(lhsn.Lane, foot, firstLaneX, eventY, lhsn.Position);

                    lastHoldStarts[lhsn.Lane]  = (int)eventY;
                    lastHoldWasRoll[lhsn.Lane] =
                        lhsn.SourceType == SMCommon.NoteChars[(int)SMCommon.NoteType.RollStart].ToString();
                }
                else if (chartEvent is LaneHoldEndNote lhen)
                {
                    if (originalChart)
                    {
                        WriteExpression(currentExpressedChartSearchNode, eventY);
                    }

                    WriteHold(lhen.Lane, firstLaneX, lastHoldStarts[lhen.Lane], eventY, lastHoldWasRoll[lhen.Lane]);
                }
                else if (chartEvent is LaneNote ln)
                {
                    if (ln.SourceType == SMCommon.NoteChars[(int)SMCommon.NoteType.Mine].ToString())
                    {
                        // Write any expressed mine events for mines at this position.
                        if (originalChart)
                        {
                            var mineEvents = new List <ExpressedChart.MineEvent>();
                            while (currentExpressedMineIndex < ExpressedChart.MineEvents.Count &&
                                   ExpressedChart.MineEvents[currentExpressedMineIndex].Position <= chartEvent.Position)
                            {
                                mineEvents.Add(ExpressedChart.MineEvents[currentExpressedMineIndex]);
                                currentExpressedMineIndex++;
                            }

                            if (mineEvents.Count > 0)
                            {
                                mineEvents = mineEvents.OrderBy(m => m.OriginalArrow).ToList();
                            }

                            WriteExpressedMines(mineEvents, eventY);
                        }

                        // Write the mine.
                        WriteMine(ln.Lane, firstLaneX, eventY, ln.Position);
                    }
                }
            }

            // Write the final measure markers
            var numMeasuresToWrite =
                chart.Layers[0].Events[chart.Layers[0].Events.Count - 1].Position.Measure
                - previousTimeSignaturePosition.Measure
                + 1;

            WriteMeasures(
                chartXPosition,
                previousTimeSignatureY,
                yPerBeat,
                currentTimeSignature,
                previousTimeSignaturePosition.Measure,
                numMeasuresToWrite,
                chart.NumInputs);
        }