public static void getEndOffsetFromObjectsByCount(Beatmap beatmap, double startOffset, int count, out double endOffset) { SearchUtils.SortBeatmapElements(beatmap.HitObjects); int index = 1; int foundIndex = -1; for (int i = 0; i < beatmap.HitObjects.Count; i++) { if (beatmap.HitObjects[i].Offset >= startOffset) { while (index < count && i++ < beatmap.HitObjects.Count) { index++; } foundIndex = i; break; } } if (foundIndex != -1) { endOffset = beatmap.HitObjects[foundIndex].Offset; } else { endOffset = -1; } }
private static void resnapElements(IEnumerable <BeatmapElement> elements, Beatmap beatmap, onFailure <Beatmap, BeatmapElement, int> listener, shouldChange <BeatmapElement> condition) { foreach (BeatmapElement hitObject in elements) { if (!condition.Invoke(hitObject)) { continue; } TimingPoint closestPoint = SearchUtils.GetClosestTimingPoint(beatmap.TimingPoints, hitObject.Offset); int closestSnappedOffset = getClosestSnappedOffset(hitObject, closestPoint, out int closestSnapValue); if (closestSnapValue != -1) { // We have a note that is equal distance to defined snaps // in the editor. Present this to the user. listener.Invoke(beatmap, hitObject, closestSnapValue); } else if (closestSnappedOffset != 0) { // The note is not snapped. We need to snap the note with // new snap value which is closestSnapInBeat + closestSnapValue. // Then the offset requires a recalculation. hitObject.Offset = closestSnappedOffset; } } }
public static bool SnapInheritedPointsOnClosestTimingPoints(Form form, Beatmap beatmap, double firstOffset, double lastOffset) { SearchUtils.GetObjectsInBetween(beatmap, firstOffset, lastOffset, out IList <TimingPoint> points); Dictionary <TimingPoint, double> newOffsets = new Dictionary <TimingPoint, double>(); bool isHighRangeDetectedAndVerified = false; foreach (TimingPoint point in points) { if (point.IsInherited) { TimingPoint closestPreviousPoint = SearchUtils.GetClosestTimingPoint(beatmap.TimingPoints, point.Offset); TimingPoint closestNextPoint = SearchUtils.GetClosestNextTimingPoint(beatmap.TimingPoints, point); double closestPreviousPointOffset = closestPreviousPoint != null ? closestPreviousPoint.Offset : 0; double closestNextPointOffset = closestNextPoint != null ? closestNextPoint.Offset : 0; double firstDifference = point.Offset - closestPreviousPointOffset; double secondDifference = closestNextPointOffset - point.Offset; if (!isHighRangeDetectedAndVerified && !VerifyUtils.verifyRangeAny(-400, 400, firstDifference, secondDifference)) { if (MessageBoxUtils.showQuestionYesNo("Inherited points with more than 400 milliseconds gap between closest timing points detected. This might result in inherited points getting completely losing their purpose.".AddLines(2) + "Are you sure you want to continue?") == DialogResult.Yes) { isHighRangeDetectedAndVerified = true; } else { return(false); } } double targetOffset; if (point.Offset - closestPreviousPointOffset < closestNextPointOffset - point.Offset) { targetOffset = closestPreviousPointOffset; } else { targetOffset = closestNextPointOffset; } newOffsets.Add(point, targetOffset); } } newOffsets.ForEach((key, value) => { key.Offset = value; }); newOffsets.Clear(); return(true); }
public static int calculateEndOffset(Beatmap beatmap, double startOffset, double gridSnap, double count) { double step = gridSnap; double totalSnap = step * count; double calculatedSnap = 0; double targetOffset = startOffset; double snapOffset = 0; while (calculatedSnap < totalSnap) { // This one has to return non-null. If it does, the exception is deserved. TimingPoint closestPoint = SearchUtils.GetClosestTimingPoint(beatmap.TimingPoints, targetOffset); // This can return null. It means we don't need to worry about this point and // calculate the offset directly. TimingPoint nextPoint = SearchUtils.GetClosestNextTimingPoint(beatmap.TimingPoints, closestPoint); snapOffset = step * closestPoint.PointValue; targetOffset += snapOffset; calculatedSnap += step; // Now, if the target offset temp passed the next point // calculate an estimated snap difference. // This is required for unsnapped timing points and a relative // end offset calculation. if (nextPoint != null && targetOffset > nextPoint.Offset) { double difference = nextPoint.Offset - targetOffset; double differenceSnap = difference / nextPoint.PointValue; // Here, we reset the target offset as the next point value // and reduce the calculated total grid snap. Step value // is not changed and the next snaps are calculated // relatively to the next point. targetOffset = nextPoint.Offset; calculatedSnap -= differenceSnap; } } // And, at the end of the day, return the target offset. return((int)targetOffset); }
public static double[] getRelativeSnap(List <TimingPoint> timingPoints, BeatmapElement target) { // First holds the true snap which is from the first point, // second holds the snap value from the closest timing point. double[] result = new double[2] { 0, 0 }; // If this is the first timing point or the element is snapped on the first timing point, // its actual and relative snaps should always equal to 0. Check the condition // and return it immediately. if (timingPoints.Count == 0 || (target.Offset == timingPoints[0].Offset && !timingPoints[0].IsInherited)) { return(result); } // Get the closest timing point to this target. TimingPoint closestPoint = SearchUtils.GetClosestTimingPoint(timingPoints, target.Offset); // Now, the beat snap divisor divides the beat to specific parts, and // we need to calculate both relative and actual snaps. // Actual snap would be closestPoint + relativeSnap while // relative snap is calculated by closest red point's point // value (a.k.a millis value between 2 beats, 60000 / BPM). // Diff of objects in millis calculated as decimal // for maximum precision. decimal diff = Convert.ToDecimal(target.Offset - closestPoint.Offset); // Relative snap value based on BEAT_SNAP_DIVISOR. decimal relativeSnap = diff * BEAT_SNAP_DIVISOR_2 / Convert.ToDecimal(closestPoint.PointValue); // Actual snap value. decimal actualSnap = Convert.ToDecimal(closestPoint.GetSnap()) + relativeSnap; // Set the results and return. First is actual snap // from the start of the map, second is relative // snap from the closest red point. result[0] = Convert.ToDouble(actualSnap); result[1] = Convert.ToDouble(relativeSnap); return(result); }
public static void changeBpmOfTimingPoint(Beatmap beatmap, HtmlDisplayer htmlDisplayer, double offset, double newValue, bool shiftRestOfBeatmap, bool saveBackups, string customPath) { if (saveBackups) { beatmap.save(customPath + "//" + beatmap.FileName); } decimal newValueDecimal = Convert.ToDecimal(newValue); // We need to extract the objects between the selected offset and the next timing point. List <TimingPoint> originalPoints = beatmap.TimingPoints; // Get exact and next timing points. Exact timing point cannot be null. If it is null, throw // a message and bail out. TimingPoint sourcePoint = SearchUtils.GetExactTimingPoint(originalPoints, offset); TimingPoint nextPoint = SearchUtils.GetClosestNextTimingPoint(originalPoints, sourcePoint); if (sourcePoint == null) { if (!htmlDisplayer.containsSections()) { htmlDisplayer.addSection("Starting uninherited timing point not found in one or more difficulties."); } else { htmlDisplayer.addLineBreak(); } htmlDisplayer.addSubsection("Beatmap difficulty: " + beatmap.DifficultyName); htmlDisplayer.addWarning(StringUtils.GetOffsetWithLink(offset) + " does not exist."); return; } // Get the objects we require. SearchUtils.GetObjectsInBetween(beatmap, offset, nextPoint != null ? nextPoint.Offset : double.MaxValue, out IList <Bookmark> bookmarks, out IList <TimingPoint> timingPoints, out IList <HitObject> hitObjects); // Since the snaps are already calculated, it should be easy to calculate the next offsets // after changing the BPM. // Remove the next timing point offset from all lists. if (nextPoint != null) { ((SubList <Bookmark>)bookmarks).TrimEnd(x => x.Offset == nextPoint.Offset); ((SubList <TimingPoint>)timingPoints).TrimEnd(x => x.Offset == nextPoint.Offset); ((SubList <HitObject>)hitObjects).TrimEnd(x => x.Offset == nextPoint.Offset); } // Next point's snap is important to determine the offset difference for rest of the objects, // if shifting is enabled. if (shiftRestOfBeatmap && nextPoint != null) { // This means there is a next point and it should be included in timingPoints list. // Check the snap differences between them and shift them all first. decimal snapDifference = Convert.ToDecimal(nextPoint.GetSnap()) - Convert.ToDecimal(sourcePoint.GetSnap()); int offsetDifference = SnapUtils.calculateEndOffsetFromBpmValue(offset, snapDifference, newValueDecimal); // Shift all the objects starting from the last object offset. SearchUtils.GetObjectsInBetween(beatmap, offset, nextPoint.Offset, out IList <Bookmark> bookmarks2, out IList <TimingPoint> timingPoints2, out IList <HitObject> hitObjects2); SnapUtils.shiftAllElementsByOffset(offsetDifference, hitObjects2, timingPoints2, bookmarks2); } // After this, now start calculating the end offsets from the relative snaps of the elements. sourcePoint.PointValue = newValue; // Use the newValueDecimal to adjust everything. SnapUtils.shiftAllElementsByNewPointValue(beatmap, sourcePoint, offset, newValueDecimal, hitObjects, timingPoints, bookmarks); // And the process should be complete. }
public static bool EqualizeSvInArea(EqualizeSvForm form, Beatmap beatmap) { // Parse the start and end offsets. double startOffset; double endOffset; if (form.EqualizeAll) { SearchUtils.SortBeatmapElements(beatmap.TimingPoints); SearchUtils.SortBeatmapElements(beatmap.HitObjects); if (beatmap.HitObjects.Count > 0) { startOffset = Math.Min(beatmap.TimingPoints.First().Offset, beatmap.HitObjects.First().Offset); } else { startOffset = beatmap.TimingPoints[0].Offset; } if (beatmap.HitObjects.Count > 0) { endOffset = Math.Max(beatmap.TimingPoints.Last().Offset, beatmap.HitObjects.Last().Offset); } else { endOffset = beatmap.TimingPoints.Last().Offset; } } else { startOffset = form.StartOffset; endOffset = form.EndOffset; } // Get the multiplier and target BPM. double targetBpm = form.TargetBpm; double svMultiplier = form.SvMultiplier == 0 ? 1 : form.SvMultiplier; // Fetch objects in between this area. // Hold the previous one as well to keep reference to add points from. TimingPoint current; TimingPoint closestRedPoint; IList <TimingPoint> points; bool originalRef = form.EqualizeAll; bool scaleWithExistingPoints = form.UseRelativeSv; if (form.EqualizeAll) { points = beatmap.TimingPoints; } else { SearchUtils.GetObjectsInBetween(beatmap, startOffset, endOffset, out points); } int startIndex = points.Count > 0 ? beatmap.TimingPoints.IndexOf(points[0]) : -1; int removeCount = points.Count; Dictionary <TimingPoint, double> originalInheritedPointValues = new Dictionary <TimingPoint, double>(); for (int i = 0; i < points.Count; i++) { current = points[i]; double offset = current.Offset; closestRedPoint = SearchUtils.GetClosestTimingPoint(beatmap.TimingPoints, offset); if (targetBpm == 0) { targetBpm = closestRedPoint.getDisplayValue(); } double baseMultiplier = targetBpm / closestRedPoint.getDisplayValue(); double finalMultiplier = baseMultiplier * svMultiplier; // If this point is an inherited point, it's no problem. Just apply the SV and continue. if (current.IsInherited) { applyMultiplierToPoint(originalInheritedPointValues, current, finalMultiplier, scaleWithExistingPoints); } else { // For timing points, we need to check the exact spot. If there is not an // inherited point with the exact offset, then we need to add it. TimingPoint exactInheritedPoint = SearchUtils.GetExactInheritedPoint(points, offset); if (exactInheritedPoint != null) { // We've found the current inherited point. Change the value and continue. applyMultiplierToPoint(originalInheritedPointValues, exactInheritedPoint, finalMultiplier, scaleWithExistingPoints); } else { // We need to add an inherited point here. Derive from the closest // timing point with the base values. TimingPoint newPoint = new TimingPoint(closestRedPoint) { IsInherited = true }; // Apply the multiplier. applyMultiplierToPoint(originalInheritedPointValues, newPoint, finalMultiplier, scaleWithExistingPoints); if (i + 1 == points.Count) { points.Add(newPoint); } else { points.Insert(i + 1, newPoint); } } } } // Sort the elements again. SearchUtils.MarkChangeMade(beatmap.TimingPoints); SearchUtils.SortBeatmapElements(beatmap.TimingPoints); return(true); }
public static bool AddSvChanges(Form form, Beatmap beatmap, double firstOffset, double lastOffset, double firstSv, double lastSv, double targetBpm, double gridSnap, int svOffset, int svIncreaseMode, int count, double svIncreaseMultiplier, bool putPointsByNotes) { List <TimingPoint> actualPoints = beatmap.TimingPoints; if (count > 0 && lastOffset <= firstOffset) { if (putPointsByNotes) { // We need to determine the end offset here from notes // and note count. SnapUtils.getEndOffsetFromObjectsByCount(beatmap, firstOffset, count, out lastOffset); } else { if (gridSnap == 0) { showErrorMessageInMainThread(form, "End offset was not defined and grid snap and count values are" + Environment.NewLine + "also not defined, hence the end offset could not be calculated. Aborting."); return(false); } lastOffset = SnapUtils.calculateEndOffset(beatmap, firstOffset, gridSnap, count); } } else if (lastOffset <= firstOffset) { showErrorMessageInMainThread(form, "End offset could not be calculated, necessary values are missing."); return(false); } SearchUtils.GetObjectsInBetween(beatmap, firstOffset, lastOffset, out IList <TimingPoint> points, out IList <HitObject> objects); // If "putPointsByNotes" is selected and no objects are found, throw an error // and return false. if (putPointsByNotes && objects.Count == 0) { showErrorMessageInMainThread(form, "No hit objects found in the specified area. Aborting."); return(false); } // We need at least 1 timing point to take reference from // the start of the list. Account that and add a timing // point if the first index is not a timing point already. if (!SearchUtils.IsFirstPointTimingPoint(points)) { points.Insert(0, SearchUtils.GetClosestTimingPoint(actualPoints, firstOffset)); } // After this, if we still don't have a timing point, we cannot proceed. // A map has to contain at least one timing point before or on the declared offset. if (!SearchUtils.ContainsTimingPoint(points)) { showErrorMessageInMainThread(form, "No timing points found to take reference from. Aborting."); return(false); } // The POW value. This is determined by // "svIncreaseMode" and "svIncreaseMultiplier" // where "svIncreaseMode" is 0 for linear, 1 for exponential // and 2 for logarithmic. If mode is 0, POW is always 1. // If mode is 1, the POW is the original value. // If mode is 2, the POW is divided as 1/POW. double pow; switch (svIncreaseMode) { case 0: pow = 1; break; case 1: pow = svIncreaseMultiplier; break; case 2: pow = 1 / svIncreaseMultiplier; break; default: throw new ArgumentException("The sv increase mode has to be 0, 1 or 2, found " + svIncreaseMode + "."); } // This is a really corner case since we already prevent it from // happening in the SV Changer form itself, but it still is checked. if (pow == 0) { showErrorMessageInMainThread(form, "The sv increase mode and sv increase multiplier has" + Environment.NewLine + "resulted in the power value being 0. Aborting."); return(false); } // If "put points by notes" is selected, // we need to calculate the target offset by // the first note. if (putPointsByNotes) { firstOffset = objects[0].Offset; } // Get the start BPM value. double startBpmValue = SearchUtils.GetBpmValueInOffset(actualPoints, firstOffset); // If "targetBpm" is entered, we need to apply a ratio to the last SV. // Apply the logic here and change the last SV value depending on it. if (targetBpm != 0) { double ratio = targetBpm / SearchUtils.GetBpmInOffset(actualPoints, firstOffset); lastSv *= ratio; } // Now that we have set already in place, let's calculate the SVs and // add or edit them. // The temp value to use on additional SVs. double targetSv; // The temp value for the SV for osu! representation. double targetSvValue; // The current percentage that will result // in the final SV of the point. This should be // always equal to percentage change if the change is linear. double targetPowerValue; // Define a target offset object and // calculate the target offset depending // on the passed parameters, a.k.a "putPointsByNoteSnaps" // or "gridSnap" and "count". double targetOffset = firstOffset; // Used in determining which hit object // we should use if "put points by notes" // is selected, as the target offset. int hitObjectIndex = 0; while (targetOffset <= lastOffset) { // Compute the actual target offset, where the offset is determined // but needs to be shifted depending on user input. double actualTargetOffset = targetOffset + svOffset; // This is the BPM value, not the BPM itself, a.k.a it is the osu! representation // of a BPM value. For instance, this is 1000 if BPM is 120 in the map, where // a beat is in a total second. double currentBpmValue = SearchUtils.GetBpmValueInOffset(points, targetOffset); // The ratio that we need to multiply while calculating the SV. double ratio = startBpmValue / currentBpmValue; // Get the closest and exact inherited points (if exists) // Closest point cannot be null (it will return the first timing point // in worst case), but exact point can be null. TimingPoint closestPoint = SearchUtils.GetClosestPoint(actualPoints, targetOffset, true); TimingPoint exactPoint = SearchUtils.GetExactInheritedPoint(actualPoints, targetOffset); // The copy point that we need to hold the attributes of. TimingPoint copy; // Determines if the object is already existing in the list. bool exists = false; // If exact point is not null, we need to edit that. if (exactPoint != null) { copy = exactPoint; exists = true; } else { copy = new TimingPoint(closestPoint) { // Make sure the copy one is inherited. IsInherited = true }; } // Get the current offset power value. This determines // how much the target SV will be as in "startSv + this value". targetPowerValue = MathUtils.calculateMultiplierFromPower(svIncreaseMultiplier, firstOffset, lastOffset, targetOffset); // Calculate the target SV. Now, the value should be divided by -100 / target SV // to achieve the osu! representation of this value. // Ratio is the BPM ratio between the start and current offset. Should be 1 // if the BPMs are the same. targetSv = MathUtils.calculateValueFromPercentage(firstSv, lastSv, targetPowerValue) * ratio; targetSvValue = -100d / targetSv; // At this point, we need to either add the point, or // replace the existing one. If the existing one toggles kiai, // and the actualTargetOffset is different from this point's offset, // we need to add a point and change the kiai one too instead. // Otherwise, we just move the point. if (exists) { if (SearchUtils.TogglesKiai(actualPoints, copy)) { // If there is an offset change (a.k.a actualTargetOffset not // equal to targetOffset) we need to add a point and change // this one as well. if (actualTargetOffset != targetOffset) { TimingPoint copy2 = new TimingPoint(copy) { Offset = actualTargetOffset, IsKiaiOpen = !copy.IsKiaiOpen, PointValue = targetSvValue }; copy.PointValue = targetSvValue; // Now, there is a trick here. If the actual inherited point // exists with the edited offset, we need to edit that, not // add a duplicate one and force an exception. // Just set "exact" values of "copy" into this. TimingPoint exact = SearchUtils.GetExactInheritedPoint(actualPoints, actualTargetOffset); if (exact != null) { exact.setTo(copy); } else { // We haven't found an exact timing point so this is fine. actualPoints.Insert(SearchUtils.GetAdditionIndex(actualPoints, copy2), copy2); } } else { copy.PointValue = targetSvValue; copy.Offset = actualTargetOffset; SearchUtils.MarkChangeMade(actualPoints); } } else { // If there is an offset change (a.k.a actualTargetOffset not // equal to targetOffset) we need to add a point and change // this one as well. if (actualTargetOffset != targetOffset) { // This time, do not change the kiai. TimingPoint copy2 = new TimingPoint(copy) { Offset = actualTargetOffset, PointValue = targetSvValue }; copy.PointValue = targetSvValue; // Now, there is a trick here. If the actual inherited point // exists with the edited offset, we need to edit that, not // add a duplicate one and force an exception. // Just set "exact" values of "copy" into this. TimingPoint exact = SearchUtils.GetExactInheritedPoint(actualPoints, actualTargetOffset); if (exact != null) { exact.setTo(copy); } else { // We haven't found an exact timing point so this is fine. actualPoints.Insert(SearchUtils.GetAdditionIndex(actualPoints, copy2), copy2); } } else { copy.PointValue = targetSvValue; copy.Offset = actualTargetOffset; SearchUtils.MarkChangeMade(actualPoints); } copy.PointValue = targetSvValue; copy.Offset = actualTargetOffset; SearchUtils.MarkChangeMade(actualPoints); } } else { copy.PointValue = targetSvValue; copy.Offset = actualTargetOffset; // Now, there is a trick here. If the actual inherited point // exists with the edited offset, we need to edit that, not // add a duplicate one and force an exception. // Just set "exact" values of "copy" into this. TimingPoint exact = SearchUtils.GetExactInheritedPoint(actualPoints, actualTargetOffset); if (exact != null) { exact.setTo(copy); } else { // We haven't found an exact timing point so this is fine. actualPoints.Insert(SearchUtils.GetAdditionIndex(actualPoints, copy), copy); } } // At the bottom, calculate the next offset // and continue. if (putPointsByNotes) { if (hitObjectIndex == objects.Count - 1) { break; } targetOffset = objects[++hitObjectIndex].Offset; } else { targetOffset += gridSnap / currentBpmValue; } } // At the end, force sort the points // and return true. actualPoints.Sort(); SearchUtils.MarkSorted(actualPoints); return(true); }