/// <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);
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); }
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); }
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};""/> "); }
/// <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; } }
/// <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; }
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); }
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};""/> "); } }
/// <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); }
/// <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}"); } } } }
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); }