/// <summary> Adds any point in time where no object in the other beatmap is within 3 ms, but /// is within the consistency range, depending on which divisor this point in time is in.<para/> /// This usually means the mapper has interpreted the same sound(s) differently from the other beatmap, /// so we add it as a potential inconsistency.</summary> private void TryAddInconsistentPlace(List <double> differenceTimes, Beatmap otherBeatmap, double otherTime) { List <double> inconsistencies = differenceTimes.Where(time => { if (Math.Abs(time - otherTime) < 3) { return(false); } UninheritedLine line = otherBeatmap.GetTimingLine <UninheritedLine>(time); double msPerBeat = line.msPerBeat; if (Math.Abs(time - otherTime) >= msPerBeat) { return(false); } double consistencyRange = GetConsistencyRange(otherBeatmap, time, msPerBeat, otherTime); return (time + consistencyRange > otherTime && time - consistencyRange < otherTime); }).ToList(); foreach (double inconsistency in inconsistencies) { if (!inconsistentPlaces.Any(place => place.Item1 == inconsistency)) { inconsistentPlaces.Add(new Tuple <double, double, Beatmap>(inconsistency, otherTime, otherBeatmap)); } } }
/// <summary> Adds any point in time where no object in the other beatmap is within 3 ms, but /// is within the consistency range, depending on which divisor this point in time is in.<para/> /// This usually means the mapper has interpreted the same sound(s) differently from the other beatmap, /// so we add it as a potential inconsistency.</summary> private void TryAddInconsistentPlace(List <double> aTimeDifferences, Beatmap anOtherBeatmap, double anOtherTime) { List <double> inconsistencies = aTimeDifferences.Where(aTime => { if (Math.Abs(aTime - anOtherTime) >= 3) { UninheritedLine line = anOtherBeatmap.GetTimingLine <UninheritedLine>(aTime); double msPerBeat = line.msPerBeat; if (Math.Abs(aTime - anOtherTime) < msPerBeat) { double consistencyRange = GetConsistencyRange(anOtherBeatmap, aTime, msPerBeat, anOtherTime); return (aTime + consistencyRange > anOtherTime && aTime - consistencyRange < anOtherTime); } } return(false); }).ToList(); foreach (double inconsistency in inconsistencies) { if (!inconsistentPlaces.Any(aPlace => aPlace.Item1 == inconsistency)) { inconsistentPlaces.Add(new Tuple <double, double, Beatmap>(inconsistency, anOtherTime, anOtherBeatmap)); } } }
/// <summary> Returns the beat number from offset 0 at which the countdown would start, accounting for /// countdown offset and speed. No countdown if less than 0. </summary> public double GetCountdownStartBeat() { // If there are no objects, this does not apply. if (GetHitObject(0) == null) { return(0); } // always 6 beats before the first, but the first beat can be cut by having the first beat 5 ms after 0. UninheritedLine line = GetTimingLine <UninheritedLine>(0); double firstBeatTime = line.offset; while (firstBeatTime - line.msPerBeat > 0) { firstBeatTime -= line.msPerBeat; } double firstObjectTime = GetHitObject(0).time; int firstObjectBeat = Timestamp.Round((firstObjectTime - firstBeatTime) / line.msPerBeat); // Apparently double does not result in the countdown needing half as much time, but rather closer to 0.45 times as much. double countdownMultiplier = generalSettings.countdown == GeneralSettings.Countdown.None ? 1 : generalSettings.countdown == GeneralSettings.Countdown.Half ? 2 : 0.45; return(firstObjectBeat - ((firstBeatTime > 5 ? 5 : 6) + generalSettings.countdownBeatOffset) * countdownMultiplier); }
public override IEnumerable <Issue> GetIssues(BeatmapSet aBeatmapSet) { Beatmap refBeatmap = aBeatmapSet.beatmaps[0]; foreach (Beatmap beatmap in aBeatmapSet.beatmaps) { foreach (TimingLine line in refBeatmap.timingLines) { if (line is UninheritedLine uninheritLine) { UninheritedLine otherUninheritLine = beatmap.timingLines.OfType <UninheritedLine>().FirstOrDefault( aLine => aLine.offset == uninheritLine.offset); double offset = Timestamp.Round(uninheritLine.offset); if (otherUninheritLine == null) { yield return(new Issue(GetTemplate("Missing"), beatmap, Timestamp.Get(offset), refBeatmap)); } else { if (uninheritLine.meter != otherUninheritLine.meter) { yield return(new Issue(GetTemplate("Inconsistent Meter"), beatmap, Timestamp.Get(offset), refBeatmap)); } if (uninheritLine.msPerBeat != otherUninheritLine.msPerBeat) { yield return(new Issue(GetTemplate("Inconsistent BPM"), beatmap, Timestamp.Get(offset), refBeatmap)); } } } } // Check the other way around as well, to make sure the reference map has all uninherited lines this map has. foreach (TimingLine line in beatmap.timingLines) { if (line is UninheritedLine) { UninheritedLine otherLine = refBeatmap.timingLines.OfType <UninheritedLine>().FirstOrDefault( aLine => aLine.offset == line.offset); double offset = (int)Math.Floor(line.offset); if (otherLine == null) { yield return(new Issue(GetTemplate("Missing"), refBeatmap, Timestamp.Get(offset), beatmap)); } } } } }
private static string RenderTicks(Beatmap aBeatmap, double aStartTime, double anEndTime) { double sampleTime = aStartTime; double prevSampleTime = aStartTime; return (String.Concat( aBeatmap.timingLines.OfType <UninheritedLine>().Select(aLine => { UninheritedLine nextLine = aBeatmap.GetNextTimingLine <UninheritedLine>(aLine.offset); double nextSwap = nextLine?.offset ?? anEndTime; StringBuilder tickDivs = new StringBuilder(); // To get precision down to both 1/16th and 1/12th of a beat we need to sample... // 16 = 2^4, 12 = 2^2*3, 2^4*3 = 48 times per beat. // We're going to intentionally ignore 1/5, 1/7, and 1/9, as we'd be sampling way too much. int samplesPerBeat = 48; for (int i = 0; i < (nextSwap - aLine.offset) / aLine.msPerBeat * samplesPerBeat; ++i) { // Add the practical unsnap to avoid things getting unsnapped the further into the map you go. sampleTime = aLine.offset + i * aLine.msPerBeat / samplesPerBeat + aBeatmap.GetPracticalUnsnap(aLine.offset + i * aLine.msPerBeat / samplesPerBeat); bool hasEdge = aBeatmap.GetHitObject(sampleTime)?.GetEdgeTimes().Any(anEdgeTime => Math.Abs(anEdgeTime - sampleTime) < 2) ?? false; if (i % (samplesPerBeat / 4) == 0 || hasEdge && ( i % (samplesPerBeat / 12) == 0 || i % (samplesPerBeat / 16) == 0)) { tickDivs.Append( DivAttr("overview-timeline-tick", " style=\"margin-left:" + ((sampleTime - prevSampleTime) / ZOOM_FACTOR) + "px\"", Div("overview-timeline-ticks-base " + (hasEdge ? " hasobject " : "") + ( i % (samplesPerBeat * aLine.meter) == 0 ? "overview-timeline-ticks-largewhite" : i % (samplesPerBeat * 1) == 0 ? "overview-timeline-ticks-white" : i % (samplesPerBeat / 2) == 0 ? "overview-timeline-ticks-red" : i % (samplesPerBeat / 3) == 0 ? "overview-timeline-ticks-magenta" : i % (samplesPerBeat / 4) == 0 ? "overview-timeline-ticks-blue" : i % (samplesPerBeat / 6) == 0 ? "overview-timeline-ticks-purple" : i % (samplesPerBeat / 8) == 0 ? "overview-timeline-ticks-yellow" : i % (samplesPerBeat / 12) == 0 ? "overview-timeline-ticks-gray" : i % (samplesPerBeat / 16) == 0 ? "overview-timeline-ticks-gray" : "overview-timeline-ticks-unsnapped") ) )); prevSampleTime = sampleTime; } } return tickDivs.ToString(); }))); }
/// <summary> Returns whether the offset aligns in such a way that one line is a multiple of 4 beats away /// from the other, and the BPM and timing signature (meter) is the same. </summary> private bool DownbeatsAlign(Beatmap beatmap, UninheritedLine line, UninheritedLine otherLine) { bool negligibleDownbeatOffset = GetBeatOffset(otherLine, line, otherLine.meter) <= 1; return (otherLine.bpm == line.bpm && otherLine.meter == line.meter && negligibleDownbeatOffset); }
/// <summary> Returns whether the offset aligns in such a way that one line is a multiple of 4 beats away /// from the other, and the BPM and timing signature (meter) is the same. </summary> private static bool DownbeatsAlign(UninheritedLine line, UninheritedLine otherLine) { bool negligibleDownbeatOffset = GetBeatOffset(otherLine, line, otherLine.meter) <= 1; return (otherLine.bpm.AlmostEqual(line.bpm) && otherLine.meter == line.meter && negligibleDownbeatOffset); }
/// <summary> Returns the ms difference between two timing lines, where the timing lines reset offset every given number of beats. </summary> private double GetBeatOffset(UninheritedLine aLine, UninheritedLine aNextLine, double aBeatOffset) { double beatsIn = (aNextLine.offset - aLine.offset) / aLine.msPerBeat; double offset = beatsIn % aBeatOffset; return (Math.Min( Math.Abs(offset), Math.Abs(offset - aBeatOffset)) * aLine.msPerBeat); }
/// <summary> Returns the ms difference between two timing lines, where the timing lines reset offset every /// given number of beats. </summary> private double GetBeatOffset(UninheritedLine line, UninheritedLine nextLine, double beatModulo) { double beatsIn = (nextLine.offset - line.offset) / line.msPerBeat; double offset = beatsIn % beatModulo; return (Math.Min( Math.Abs(offset), Math.Abs(offset - beatModulo)) * line.msPerBeat); }
/// <summary> Returns how many ms into a beat the given time is. </summary> public double GetOffsetIntoBeat(double aTime) { UninheritedLine line = GetTimingLine <UninheritedLine>(aTime); // gets how many miliseconds into a beat we are double time = aTime - line.offset; double division = time / line.msPerBeat; double fraction = division - (float)Math.Floor(division); double beatOffset = fraction * line.msPerBeat; return(beatOffset); }
private string GetSnappingGap(Beatmap beatmap, HitObject hitObject) { HitObject previousObject = hitObject.PrevOrFirst(); double lastObjectTime = previousObject.GetEdgeTimes().Last(); double snappedCurrentObject = hitObject.time + beatmap.GetPracticalUnsnap(hitObject.time); double snappedPreviousObject = lastObjectTime + beatmap.GetPracticalUnsnap(lastObjectTime); double deltaTime = snappedCurrentObject - snappedPreviousObject; UninheritedLine timingLine = beatmap.GetTimingLine <UninheritedLine>(snappedCurrentObject); var snapping = Math.Round(deltaTime / timingLine.msPerBeat, 2); var snappingStr = new Fraction(snapping).ToString(); return(snappingStr); }
private IEnumerable <Issue> GetRecoveryIssues(Beatmap aBeatmap, Spinner aSpinner) { HitObject nextObject = aBeatmap.GetNextHitObject(aSpinner.time); // Do not check time between two spinners since all you'd need to do is keep spinning. if (nextObject != null && !(nextObject is Spinner)) { double recoveryTime = nextObject.time - aSpinner.endTime; UninheritedLine line = aBeatmap.GetTimingLine <UninheritedLine>(nextObject.time); double bpmScaling = GetScaledTiming(line.bpm); double recoveryTimeScaled = recoveryTime / bpmScaling; double[] recoveryTimeExpected = new double[] { 1000, 500, 250 }; // 4, 2 and 1 beats respectively, 240 bpm // Tries both scaled and regular recoveries, and only if both are exceeded does it create an issue. for (int diffIndex = 0; diffIndex < recoveryTimeExpected.Length; ++diffIndex) { // Picks whichever is greatest of the scaled and regular versions. double expectedScaledMultiplier = (bpmScaling < 1 ? bpmScaling : 1); double expectedRecovery = Math.Ceiling(recoveryTimeExpected[diffIndex] * expectedScaledMultiplier * expectedMultiplier); double problemThreshold = recoveryTimeExpected[diffIndex]; double warningThreshold = recoveryTimeExpected[diffIndex] * 1.2; if (recoveryTimeScaled < problemThreshold && recoveryTime < problemThreshold) { yield return(new Issue(GetTemplate("Problem Recovery"), aBeatmap, Timestamp.Get(aSpinner, nextObject), recoveryTime, expectedRecovery) .ForDifficulties((Beatmap.Difficulty)diffIndex)); } else if (recoveryTimeScaled < warningThreshold && recoveryTime < warningThreshold) { yield return(new Issue(GetTemplate("Warning Recovery"), aBeatmap, Timestamp.Get(aSpinner, nextObject), recoveryTime, expectedRecovery) .ForDifficulties((Beatmap.Difficulty)diffIndex)); } } } }
/// <summary> Returns the unsnap ignoring all of the game's rounding and other approximations. </summary> public double GetTheoreticalUnsnap(double aTime, int aSecondDivisor = 16, int aThirdDivisor = 12) { UninheritedLine line = GetTimingLine <UninheritedLine>(aTime); double beatOffset = GetOffsetIntoBeat(aTime); double currentFraction = beatOffset / line.msPerBeat; // 1/16 double desiredFractionSecond = (float)Math.Round(currentFraction * aSecondDivisor) / aSecondDivisor; double differenceFractionSecond = currentFraction - desiredFractionSecond; double theoreticalUnsnapSecond = differenceFractionSecond * line.msPerBeat; // 1/12 double desiredFractionThird = (float)Math.Round(currentFraction * aThirdDivisor) / aThirdDivisor; double differenceFractionThird = currentFraction - desiredFractionThird; double theoreticalUnsnapThird = differenceFractionThird * line.msPerBeat; // picks the smaller of the two as unsnap return(Math.Abs(theoreticalUnsnapThird) > Math.Abs(theoreticalUnsnapSecond) ? theoreticalUnsnapSecond : theoreticalUnsnapThird); }
public override IEnumerable <Issue> GetIssues(BeatmapSet beatmapSet) { IEnumerable <Beatmap> taikoBeatmaps = beatmapSet.beatmaps.Where(beatmap => beatmap.generalSettings.mode == Beatmap.Mode.Taiko); Beatmap refBeatmap = taikoBeatmaps.First(); foreach (Beatmap beatmap in taikoBeatmaps) { foreach (UninheritedLine line in refBeatmap.timingLines.OfType <UninheritedLine>()) { UninheritedLine respectiveLine = beatmap.timingLines.OfType <UninheritedLine>().FirstOrDefault( otherLine => Timestamp.Round(otherLine.offset) == Timestamp.Round(line.offset)); double offset = Timestamp.Round(line.offset); if (line.omitsBarLine != respectiveLine.omitsBarLine) { yield return(new Issue(GetTemplate("Inconsistent"), beatmap, Timestamp.Get(offset), refBeatmap)); } } } }
public override IEnumerable <Issue> GetIssues(Beatmap aBeatmap) { // Since the list of timing lines is sorted by time we can just check the previous line. for (int i = 1; i < aBeatmap.timingLines.Count; ++i) { if (aBeatmap.timingLines[i - 1].offset == aBeatmap.timingLines[i].offset) { if (aBeatmap.timingLines[i - 1].uninherited == aBeatmap.timingLines[i].uninherited) { string inheritance = aBeatmap.timingLines[i].uninherited ? "uninherited" : "inherited"; yield return(new Issue(GetTemplate("Concurrent"), aBeatmap, Timestamp.Get(aBeatmap.timingLines[i].offset), inheritance)); } else if ( aBeatmap.timingLines[i - 1].kiai != aBeatmap.timingLines[i].kiai || aBeatmap.timingLines[i - 1].volume != aBeatmap.timingLines[i].volume || aBeatmap.timingLines[i - 1].sampleset != aBeatmap.timingLines[i].sampleset || aBeatmap.timingLines[i - 1].customIndex != aBeatmap.timingLines[i].customIndex) { string conflictingGreenSettings = ""; string conflictingRedSettings = ""; InheritedLine greenLine = null; UninheritedLine redLine = null; // We've guaranteed that one line is inherited and the other is // uninherited, so we can figure out both by checking one. string precedence = ""; if (aBeatmap.timingLines[i - 1] is InheritedLine) { greenLine = aBeatmap.timingLines[i - 1] as InheritedLine; redLine = aBeatmap.timingLines[i] as UninheritedLine; precedence = "Red overrides green"; } else { greenLine = aBeatmap.timingLines[i] as InheritedLine; redLine = aBeatmap.timingLines[i - 1] as UninheritedLine; precedence = "Green overrides red"; } if (greenLine.kiai != redLine.kiai) { conflictingGreenSettings += (conflictingGreenSettings.Length > 0 ? ", " : "") + (greenLine.kiai ? "kiai" : "no kiai"); conflictingRedSettings += (conflictingRedSettings.Length > 0 ? ", " : "") + (redLine.kiai ? "kiai" : "no kiai"); } if (greenLine.volume != redLine.volume) { conflictingGreenSettings += (conflictingGreenSettings.Length > 0 ? ", " : "") + $"{greenLine.volume}% volume"; conflictingRedSettings += (conflictingRedSettings.Length > 0 ? ", " : "") + $"{redLine.volume}% volume"; } if (greenLine.sampleset != redLine.sampleset) { conflictingGreenSettings += (conflictingGreenSettings.Length > 0 ? ", " : "") + $"{greenLine.sampleset} sampleset"; conflictingRedSettings += (conflictingRedSettings.Length > 0 ? ", " : "") + $"{redLine.sampleset} sampleset"; } if (greenLine.customIndex != redLine.customIndex) { conflictingGreenSettings += (conflictingGreenSettings.Length > 0 ? ", " : "") + $"custom {greenLine.customIndex}"; conflictingRedSettings += (conflictingRedSettings.Length > 0 ? ", " : "") + $"custom {redLine.customIndex}"; } yield return(new Issue(GetTemplate("Conflicting"), aBeatmap, Timestamp.Get(aBeatmap.timingLines[i].offset), conflictingGreenSettings, conflictingRedSettings, precedence)); } } } }
/// <summary> Returns whether the offset aligns in such a way that one line is a multiple of 4 measures away /// from the other (1 measure = 4 beats in 4/4 meter). This first checks that the downbeat structure is the same. /// <br></br><br></br> /// In the Nightcore mod, cymbals can be heard every 4 measures. </summary> private bool NightcoreCymbalsAlign(Beatmap beatmap, UninheritedLine line, UninheritedLine otherLine) => DownbeatsAlign(beatmap, line, otherLine) && GetBeatOffset(otherLine, line, 4 * otherLine.meter) <= 1;
/// <summary> Returns whether the bar lines from the first line align perfectly with those of the second. /// Assumes the two lines have identical BPM and meter, use <see cref="DownbeatsAlign"/> for that. </summary> private bool BarLinesAlign(Beatmap beatmap, UninheritedLine line, UninheritedLine otherLine) => // Even differences in 1 ms would be visible since it'd make 2 barlines next to each other. GetBeatOffset(otherLine, line, otherLine.meter) == 0;
/// <summary> Returns whether the offset aligns in such a way that one line is a multiple of 4 measures away /// from the other (1 measure = 4 beats in 4/4 meter). This first checks that the downbeat structure is the same. /// <br></br><br></br> /// In the Nightcore mod, cymbals can be heard every 4 measures. </summary> private static bool NightcoreCymbalsAlign(UninheritedLine line, UninheritedLine otherLine) => DownbeatsAlign(line, otherLine) && GetBeatOffset(otherLine, line, 4 * otherLine.meter) <= 1;
private IEnumerable <Issue> GetUninheritedLineIssues(Beatmap beatmap) { List <TimingLine> lines = beatmap.timingLines.ToList(); for (int i = 1; i < lines.Count; ++i) { if (!(lines[i] is UninheritedLine currentLine)) { continue; } // Can't do lines[i - 1] since that could give a green line on the same offset, which we don't want. TimingLine previousLine = beatmap.GetTimingLine(currentLine.offset - 1); UninheritedLine previousUninheritedLine = beatmap.GetTimingLine <UninheritedLine>(currentLine.offset - 1); if (!DownbeatsAlign(beatmap, currentLine, previousUninheritedLine)) { continue; } bool changesNCCymbals = false; if (!NightcoreCymbalsAlign(beatmap, currentLine, previousUninheritedLine)) { changesNCCymbals = true; } bool omittingBarline = false; bool correctingBarline = false; if (CanOmitBarLine(beatmap)) { // e.g. red line used mid-measure to account for bpm change shouldn't create a barline, so it's omitted, but the // end of the measure won't have a barline unless another red line is placed there to correct it, hence both used. omittingBarline = currentLine.omitsBarLine; correctingBarline = previousUninheritedLine.omitsBarLine && !BarLinesAlign(beatmap, currentLine, previousUninheritedLine); // Omitting bar lines isn't commonly seen in standard, so it's likely that people will // miss incorrect usages of it, hence warn if it's the only thing keeping it used. if ((omittingBarline || correctingBarline) && beatmap.generalSettings.mode != Beatmap.Mode.Standard) { continue; } } List <string> notImmediatelyObvious = new List <string>(); if (omittingBarline) { notImmediatelyObvious.Add("omitting first barline"); } if (correctingBarline) { notImmediatelyObvious.Add($"correcting the omitted barline at {Timestamp.Get(previousUninheritedLine.offset)}"); } if (changesNCCymbals) { notImmediatelyObvious.Add("nightcore mod cymbals"); } string notImmediatelyObviousStr = string.Join(" and ", notImmediatelyObvious); if (!IsLineUsed(beatmap, currentLine, previousLine)) { if (notImmediatelyObvious.Count == 0) { yield return(new Issue(GetTemplate("Problem"), beatmap, Timestamp.Get(currentLine.offset))); } else { yield return(new Issue(GetTemplate("Warning"), beatmap, Timestamp.Get(currentLine.offset), notImmediatelyObviousStr)); } } else { if (notImmediatelyObvious.Count == 0) { yield return(new Issue(GetTemplate("Problem Inherited"), beatmap, Timestamp.Get(currentLine.offset))); } else { yield return(new Issue(GetTemplate("Warning Inherited"), beatmap, Timestamp.Get(currentLine.offset), notImmediatelyObviousStr)); } } } }
public override IEnumerable <Issue> GetIssues(BeatmapSet beatmapSet) { Beatmap refBeatmap = beatmapSet.beatmaps[0]; foreach (Beatmap beatmap in beatmapSet.beatmaps) { foreach (UninheritedLine line in refBeatmap.timingLines.OfType <UninheritedLine>()) { UninheritedLine respectiveLine = beatmap.timingLines.OfType <UninheritedLine>().FirstOrDefault( otherLine => Timestamp.Round(otherLine.offset) == Timestamp.Round(line.offset)); double offset = Timestamp.Round(line.offset); if (respectiveLine == null) { yield return(new Issue(GetTemplate("Missing"), beatmap, Timestamp.Get(offset), refBeatmap)); } else { if (line.meter != respectiveLine.meter) { yield return(new Issue(GetTemplate("Inconsistent Meter"), beatmap, Timestamp.Get(offset), refBeatmap)); } if (line.msPerBeat != respectiveLine.msPerBeat) { yield return(new Issue(GetTemplate("Inconsistent BPM"), beatmap, Timestamp.Get(offset), refBeatmap)); } // Including decimal unsnaps UninheritedLine respectiveLineExact = beatmap.timingLines.OfType <UninheritedLine>().FirstOrDefault( otherLine => otherLine.offset == line.offset); if (respectiveLineExact == null) { yield return(new Issue(GetTemplate("Missing Minor"), beatmap, Timestamp.Get(offset), refBeatmap)); } } } // Check the other way around as well, to make sure the reference map has all uninherited lines this map has. foreach (UninheritedLine line in beatmap.timingLines.OfType <UninheritedLine>()) { UninheritedLine respectiveLine = refBeatmap.timingLines.OfType <UninheritedLine>().FirstOrDefault( otherLine => Timestamp.Round(otherLine.offset) == Timestamp.Round(line.offset)); double offset = Timestamp.Round(line.offset); if (respectiveLine == null) { yield return(new Issue(GetTemplate("Missing"), refBeatmap, Timestamp.Get(offset), beatmap)); } else { // Including decimal unsnaps UninheritedLine respectiveLineExact = refBeatmap.timingLines.OfType <UninheritedLine>().FirstOrDefault( otherLine => otherLine.offset == line.offset); if (respectiveLineExact == null) { yield return(new Issue(GetTemplate("Missing Minor"), refBeatmap, Timestamp.Get(offset), beatmap)); } } } } }
public override IEnumerable <DiffInstance> Translate(IEnumerable <DiffInstance> aDiffs) { List <Tuple <DiffInstance, TimingLine> > addedTimingLines = new List <Tuple <DiffInstance, TimingLine> >(); List <Tuple <DiffInstance, TimingLine> > removedTimingLines = new List <Tuple <DiffInstance, TimingLine> >(); foreach (DiffInstance diff in aDiffs) { TimingLine timingLine = null; try { timingLine = new TimingLine(diff.difference.Split(','), beatmap: null); } catch { // Failing to parse a changed line shouldn't stop it from showing. } if (timingLine != null) { if (diff.diffType == DiffType.Added) { addedTimingLines.Add(new Tuple <DiffInstance, TimingLine>(diff, timingLine)); } else { removedTimingLines.Add(new Tuple <DiffInstance, TimingLine>(diff, timingLine)); } } else { // Shows the raw .osu line change. yield return(diff); } } foreach (Tuple <DiffInstance, TimingLine> addedTuple in addedTimingLines) { DiffInstance addedDiff = addedTuple.Item1; TimingLine addedLine = addedTuple.Item2; string stamp = Timestamp.Get(addedLine.offset); string type = addedLine.uninherited ? "Uninherited line" : "Inherited line"; bool found = false; foreach (TimingLine removedLine in removedTimingLines.Select(aTuple => aTuple.Item2).ToList()) { if (!addedLine.offset.AlmostEqual(removedLine.offset)) { continue; } string removedType = removedLine.uninherited ? "Uninherited line" : "Inherited line"; if (type != removedType) { continue; } List <string> changes = new List <string>(); if (addedLine.kiai != removedLine.kiai) { changes.Add("Kiai changed from " + (removedLine.kiai ? "enabled" : "disabled") + " to " + (addedLine.kiai ? "enabled" : "disabled") + "."); } if (addedLine.meter != removedLine.meter) { changes.Add("Timing signature changed from " + removedLine.meter + "/4" + " to " + addedLine.meter + "/4."); } if (addedLine.sampleset != removedLine.sampleset) { changes.Add("Sampleset changed from " + removedLine.sampleset.ToString().ToLower() + " to " + addedLine.sampleset.ToString().ToLower() + "."); } if (addedLine.customIndex != removedLine.customIndex) { changes.Add("Custom sampleset index changed from " + removedLine.customIndex.ToString().ToLower() + " to " + addedLine.customIndex.ToString().ToLower() + "."); } if (!addedLine.volume.AlmostEqual(removedLine.volume)) { changes.Add("Volume changed from " + removedLine.volume + " to " + addedLine.volume + "."); } if (type == "Uninherited line") { UninheritedLine addedUninherited = new UninheritedLine(addedLine.code.Split(','), beatmap: null); UninheritedLine removedUninherited = new UninheritedLine(removedLine.code.Split(','), beatmap: null); if (!addedUninherited.bpm.AlmostEqual(removedUninherited.bpm)) { changes.Add("BPM changed from " + removedUninherited.bpm + " to " + addedUninherited.bpm + "."); } } else if (!addedLine.svMult.AlmostEqual(removedLine.svMult)) { changes.Add("Slider velocity multiplier changed from " + removedLine.svMult + " to " + addedLine.svMult + "."); } if (changes.Count == 1) { yield return(new DiffInstance(stamp + changes[0], Section, DiffType.Changed, new List <string>(), addedDiff.snapshotCreationDate)); } else if (changes.Count > 1) { yield return(new DiffInstance(stamp + type + " changed.", Section, DiffType.Changed, changes, addedDiff.snapshotCreationDate)); } found = true; removedTimingLines.RemoveAll(aTuple => aTuple.Item2.code == removedLine.code); } if (!found) { yield return(new DiffInstance(stamp + type + " added.", Section, DiffType.Added, new List <string>(), addedDiff.snapshotCreationDate)); } } foreach (Tuple <DiffInstance, TimingLine> removedTuple in removedTimingLines) { DiffInstance removedDiff = removedTuple.Item1; TimingLine removedLine = removedTuple.Item2; string stamp = Timestamp.Get(removedLine.offset); string type = removedLine.uninherited ? "Uninherited line" : "Inherited line"; yield return(new DiffInstance(stamp + type + " removed.", Section, DiffType.Removed, new List <string>(), removedDiff.snapshotCreationDate)); } }
public override IEnumerable <Issue> GetIssues(Beatmap aBeatmap) { foreach (HitObject hitObject in aBeatmap.hitObjects) { if (hitObject is Spinner spinner) { HitObject nextObject = aBeatmap.GetNextHitObject(hitObject.time); // Don't check time between two spinners since all you'd need to do is keep spinning. while (nextObject != null && nextObject is Spinner) { nextObject = aBeatmap.GetNextHitObject(nextObject.time); } if (nextObject != null) { double spinnerTime = spinner.endTime - spinner.time; double recoveryTime = nextObject.time - spinner.endTime; UninheritedLine line = aBeatmap.GetTimingLine <UninheritedLine>(nextObject.time); double bpmScaling = GetScaledTiming(line.bpm); double recoveryTimeScaled = recoveryTime / bpmScaling; // Equal to the ms length of a beat in 180 BPM divided by the same thing in 240 BPM. // So when multiplied by the expected time, we get the time that the Ranking Criteria wanted, which is based on 180 BPM. double expectedMultiplier = 4 / 3d; double[] spinnerTimeExpected = new double[] { 1000, 500, 250 }; // 4, 2 and 1 beats respectively, 240 bpm for (int diffIndex = 0; diffIndex < spinnerTimeExpected.Length; ++diffIndex) { if (spinnerTime < spinnerTimeExpected[diffIndex]) { yield return(new Issue(GetTemplate("Problem Length"), aBeatmap, Timestamp.Get(spinner), spinnerTime, Math.Ceiling(spinnerTimeExpected[diffIndex] * expectedMultiplier)) .ForDifficulties((Beatmap.Difficulty)diffIndex)); } else if (spinnerTime < spinnerTimeExpected[diffIndex] * 1.2) // same thing but 200 bpm instead { yield return(new Issue(GetTemplate("Warning Length"), aBeatmap, Timestamp.Get(spinner), spinnerTime, Math.Ceiling(spinnerTimeExpected[diffIndex] * expectedMultiplier)) .ForDifficulties((Beatmap.Difficulty)diffIndex)); } } double[] recoveryTimeExpected = new double[] { 1000, 500, 250 }; // 4, 2 and 1 beats respectively, 240 bpm // Tries both scaled and regular recoveries, and only if both are exceeded does it create an issue. for (int diffIndex = 0; diffIndex < recoveryTimeExpected.Length; ++diffIndex) { // Picks whichever is greatest of the scaled and regular versions. double expectedScaledMultiplier = (bpmScaling < 1 ? bpmScaling : 1); if (recoveryTimeScaled < recoveryTimeExpected[diffIndex] && recoveryTime < recoveryTimeExpected[diffIndex]) { yield return(new Issue(GetTemplate("Problem Recovery"), aBeatmap, Timestamp.Get(spinner, nextObject), recoveryTime, Math.Ceiling(recoveryTimeExpected[diffIndex] * expectedScaledMultiplier * expectedMultiplier)) .ForDifficulties((Beatmap.Difficulty)diffIndex)); } else if (recoveryTimeScaled < recoveryTimeExpected[diffIndex] * 1.2 && recoveryTime < recoveryTimeExpected[diffIndex] * 1.2) { yield return(new Issue(GetTemplate("Warning Recovery"), aBeatmap, Timestamp.Get(spinner, nextObject), recoveryTime, Math.Ceiling(recoveryTimeExpected[diffIndex] * expectedScaledMultiplier * expectedMultiplier)) .ForDifficulties((Beatmap.Difficulty)diffIndex)); } } } } } }