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