public static void ExportHitsounds(IEnumerable <HitsoundEvent> hitsounds, string baseBeatmap, string exportFolder, bool useGreenlines = true, bool useStoryboard = false) { EditorReaderStuff.TryGetNewestVersion(baseBeatmap, out var editor); Beatmap beatmap = editor.Beatmap; if (useStoryboard) { beatmap.StoryboardSoundSamples = hitsounds.Select(h => new StoryboardSoundSample(h.Time, 0, h.Filename, h.Volume)) .ToList(); } else { // Make new timing points // Add red lines List <TimingPoint> timingPoints = beatmap.BeatmapTiming.GetAllRedlines(); List <TimingPointsChange> timingPointsChanges = timingPoints.Select(tp => new TimingPointsChange(tp, mpb: true, meter: true, inherited: true, omitFirstBarLine: true)) .ToList(); // Add hitsound stuff // Replace all hitobjects with the hitsounds beatmap.HitObjects.Clear(); foreach (HitsoundEvent h in hitsounds) { if (useGreenlines) { TimingPoint tp = beatmap.BeatmapTiming.GetTimingPointAtTime(h.Time + 5).Copy(); tp.Offset = h.Time; tp.SampleIndex = h.CustomIndex; h.CustomIndex = 0; // Set it to default value because it gets handled by greenlines now tp.Volume = Math.Round(tp.Volume * h.Volume); h.Volume = 0; // Set it to default value because it gets handled by greenlines now timingPointsChanges.Add(new TimingPointsChange(tp, index: true, volume: true)); } beatmap.HitObjects.Add(new HitObject(h.Pos, h.Time, 5, h.GetHitsounds(), h.SampleSet, h.Additions, h.CustomIndex, h.Volume * 100, h.Filename)); } // Replace the old timingpoints beatmap.BeatmapTiming.TimingPoints.Clear(); TimingPointsChange.ApplyChanges(beatmap.BeatmapTiming, timingPointsChanges); } // Change version to hitsounds beatmap.General["StackLeniency"] = new TValue("0.0"); beatmap.General["Mode"] = new TValue("0"); beatmap.Metadata["Version"] = new TValue("Hitsounds"); beatmap.Difficulty["CircleSize"] = new TValue("4"); // Save the file to the export folder editor.SaveFile(Path.Combine(exportFolder, beatmap.GetFileName())); }
public static void ExportHitsounds(List<HitsoundEvent> hitsounds, string baseBeatmap, string exportFolder) { EditorReaderStuff.TryGetNewestVersion(baseBeatmap, out var editor); Beatmap beatmap = editor.Beatmap; // Make new timing points List<TimingPointsChange> timingPointsChanges = new List<TimingPointsChange>(); // Add red lines List<TimingPoint> timingPoints = beatmap.BeatmapTiming.GetAllRedlines(); foreach (TimingPoint tp in timingPoints) { timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true, meter: true, inherited: true, omitFirstBarLine: true)); } // Add hitsound stuff foreach (HitsoundEvent h in hitsounds) { TimingPoint tp = beatmap.BeatmapTiming.GetTimingPointAtTime(h.Time + 5).Copy(); tp.Offset = h.Time; tp.SampleIndex = h.CustomIndex; tp.Volume = Math.Round(tp.Volume * h.Volume); timingPointsChanges.Add(new TimingPointsChange(tp, index: true, volume: true)); } // Replace the old timingpoints beatmap.BeatmapTiming.TimingPoints.Clear(); TimingPointsChange.ApplyChanges(beatmap.BeatmapTiming, timingPointsChanges); // Replace all hitobjects with the hitsounds beatmap.HitObjects.Clear(); foreach (HitsoundEvent h in hitsounds) { beatmap.HitObjects.Add(new HitObject(h.Time, h.GetHitsounds(), h.SampleSet, h.Additions)); } // Change version to hitsounds beatmap.General["StackLeniency"] = new TValue("0.0"); beatmap.General["Mode"] = new TValue("0"); beatmap.Metadata["Version"] = new TValue("Hitsounds"); beatmap.Difficulty["CircleSize"] = new TValue("4"); // Save the file to the export folder editor.SaveFile(Path.Combine(exportFolder, beatmap.GetFileName())); }
private string Copy_Timing(TimingCopierVm arg, BackgroundWorker worker, DoWorkEventArgs _) { string[] paths = arg.ExportPath.Split('|'); int mapsDone = 0; var reader = EditorReaderStuff.GetFullEditorReaderOrNot(); foreach (string exportPath in paths) { var editorTo = EditorReaderStuff.GetNewestVersionOrNot(exportPath, reader); var editorFrom = EditorReaderStuff.GetNewestVersionOrNot(arg.ImportPath, reader); Beatmap beatmapTo = editorTo.Beatmap; Beatmap beatmapFrom = editorFrom.Beatmap; Timing timingTo = beatmapTo.BeatmapTiming; Timing timingFrom = beatmapFrom.BeatmapTiming; // Get markers for hitobjects if mode 1 is used List <Marker> markers = new List <Marker>(); if (arg.ResnapMode == "Number of beats between objects stays the same") { markers = GetMarkers(beatmapTo, timingTo); } // Rid the beatmap of redlines // If a greenline exists at the same time as a redline then the redline ceizes to exist // Else convert the redline to a greenline: Inherited = false & MpB = -100 List <TimingPoint> removeList = new List <TimingPoint>(); foreach (TimingPoint redline in timingTo.GetAllRedlines()) { TimingPoint greenlineHere = timingTo.GetGreenlineAtTime(redline.Offset); if (greenlineHere.Offset == redline.Offset) { removeList.Add(redline); } else { redline.Uninherited = false; redline.MpB = -100; } } foreach (TimingPoint tp in removeList) { timingTo.TimingPoints.Remove(tp); } // Make new timing points changes List <TimingPointsChange> timingPointsChanges = new List <TimingPointsChange>(); // Add redlines List <TimingPoint> redlines = timingFrom.GetAllRedlines(); foreach (TimingPoint tp in redlines) { timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true, meter: true, inherited: true, omitFirstBarLine: true)); } // Apply timing changes TimingPointsChange.ApplyChanges(timingTo, timingPointsChanges); if (arg.ResnapMode == "Number of beats between objects stays the same") { redlines = timingTo.GetAllRedlines(); List <double> newBookmarks = new List <double>(); double lastTime = redlines.FirstOrDefault().Offset; foreach (Marker marker in markers) { // Get redlines between this and last marker TimingPoint redline = timingTo.GetRedlineAtTime(lastTime, redlines.FirstOrDefault()); double beatsFromLastTime = marker.BeatsFromLastMarker; while (true) { List <TimingPoint> redlinesBetween = redlines.Where(o => o.Offset <= lastTime + redline.MpB * beatsFromLastTime && o.Offset > lastTime).ToList(); if (redlinesBetween.Count == 0) { break; } TimingPoint first = redlinesBetween.First(); double diff = first.Offset - lastTime; beatsFromLastTime -= diff / redline.MpB; redline = first; lastTime = first.Offset; } // Last time is the time of the last redline in between double newTime = lastTime + redline.MpB * beatsFromLastTime; newTime = timingTo.Resnap(newTime, arg.Snap1, arg.Snap2, firstTp: redlines.FirstOrDefault()); marker.Time = newTime; lastTime = marker.Time; } // Add the bookmarks foreach (Marker marker in markers) { // Check whether the marker is a bookmark if (marker.Object is double) { // Don't resnap bookmarks newBookmarks.Add((double)marker.Object); } } beatmapTo.SetBookmarks(newBookmarks); } else if (arg.ResnapMode == "Just resnap") { // Resnap hitobjects foreach (HitObject ho in beatmapTo.HitObjects) { ho.ResnapSelf(timingTo, arg.Snap1, arg.Snap2, firstTp: redlines.FirstOrDefault()); ho.ResnapEnd(timingTo, arg.Snap1, arg.Snap2, firstTp: redlines.FirstOrDefault()); } // Resnap greenlines foreach (TimingPoint tp in timingTo.GetAllGreenlines()) { tp.ResnapSelf(timingTo, arg.Snap1, arg.Snap2, firstTP: redlines.FirstOrDefault()); } } else { // Don't move objects } // Save the file editorTo.SaveFile(); // Update progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(++mapsDone * 100 / paths.Length); } } // Make an accurate message string message = $"Successfully copied timing to {mapsDone} {(mapsDone == 1 ? "beatmap" : "beatmaps")}!"; return(message); }
private string Copy_Hitsounds(HitsoundCopierVm arg, BackgroundWorker worker) { var doMutedIndex = arg.MutedIndex >= 0; var paths = arg.PathTo.Split('|'); var mapsDone = 0; var sampleSchema = new SampleSchema(); var reader = EditorReaderStuff.GetFullEditorReaderOrNot(); foreach (var pathTo in paths) { BeatmapEditor editorTo = EditorReaderStuff.GetNewestVersionOrNot(pathTo, reader);; Beatmap beatmapTo = editorTo.Beatmap; Beatmap beatmapFrom; if (!string.IsNullOrEmpty(arg.PathFrom)) { var editorFrom = EditorReaderStuff.GetNewestVersionOrNot(arg.PathFrom, reader); beatmapFrom = editorFrom.Beatmap; } else { // Copy from an empty beatmap similar to the map to copy to beatmapFrom = beatmapTo.DeepCopy(); beatmapFrom.HitObjects.Clear(); beatmapFrom.BeatmapTiming.Clear(); } Timeline processedTimeline; if (arg.CopyMode == 0) { // Every defined hitsound and sampleset on hitsound gets copied to their copyTo destination // Timelines var tlTo = beatmapTo.GetTimeline(); var tlFrom = beatmapFrom.GetTimeline(); var volumeMuteTimes = arg.CopyVolumes && arg.AlwaysPreserve5Volume ? new List <double>() : null; if (arg.CopyHitsounds) { ResetHitObjectHitsounds(beatmapTo); CopyHitsounds(arg, tlFrom, tlTo); } // Save tlo times where timingpoint volume is 5% // Timingpointchange all the undefined tlo from copyFrom volumeMuteTimes?.AddRange(from tloTo in tlTo.TimelineObjects where tloTo.CanCopy && Math.Abs(tloTo.SampleVolume) < Precision.DOUBLE_EPSILON && Math.Abs(tloTo.FenoSampleVolume - 5) < Precision.DOUBLE_EPSILON select tloTo.Time); // Volumes and samplesets and customindices greenlines get copied with timingpointchanges and allafter enabled var timingPointsChanges = beatmapFrom.BeatmapTiming.TimingPoints.Select(tp => new TimingPointsChange(tp, sampleset: arg.CopySampleSets, index: arg.CopySampleSets, volume: arg.CopyVolumes)).ToList(); // Apply the timingpoint changes TimingPointsChange.ApplyChanges(beatmapTo.BeatmapTiming, timingPointsChanges, true); processedTimeline = tlTo; // Return 5% volume to tlo that had it before if (volumeMuteTimes != null) { var timingPointsChangesMute = new List <TimingPointsChange>(); processedTimeline.GiveTimingPoints(beatmapTo.BeatmapTiming); // Exclude objects which use their own sample volume property instead foreach (var tloTo in processedTimeline.TimelineObjects.Where(o => Math.Abs(o.SampleVolume) < Precision.DOUBLE_EPSILON)) { if (volumeMuteTimes.Contains(tloTo.Time)) { // Add timingpointschange to copy timingpoint hitsounds var tp = tloTo.HitsoundTimingPoint.Copy(); tp.Offset = tloTo.Time; tp.Volume = 5; timingPointsChangesMute.Add(new TimingPointsChange(tp, volume: true)); } else { // Add timingpointschange to preserve index and volume var tp = tloTo.HitsoundTimingPoint.Copy(); tp.Offset = tloTo.Time; tp.Volume = tloTo.FenoSampleVolume; timingPointsChangesMute.Add(new TimingPointsChange(tp, volume: true)); } } // Apply the timingpoint changes TimingPointsChange.ApplyChanges(beatmapTo.BeatmapTiming, timingPointsChangesMute); } } else { // Smarty mode // Copy the defined hitsounds literally (not feno, that will be reserved for cleaner). Only the tlo that have been defined by copyFrom get overwritten. var tlTo = beatmapTo.GetTimeline(); var tlFrom = beatmapFrom.GetTimeline(); var timingPointsChanges = new List <TimingPointsChange>(); var mode = (GameMode)beatmapTo.General["Mode"].IntValue; var mapDir = editorTo.GetParentFolder(); var firstSamples = HitsoundImporter.AnalyzeSamples(mapDir); if (arg.CopyHitsounds) { CopyHitsounds(arg, beatmapTo, tlFrom, tlTo, timingPointsChanges, mode, mapDir, firstSamples, ref sampleSchema); } if (arg.CopyBodyHitsounds) { // Remove timingpoints in beatmapTo that are in a sliderbody/spinnerbody for both beatmapTo and BeatmapFrom foreach (var tp in from ho in beatmapTo.HitObjects from tp in ho.BodyHitsounds where beatmapFrom.HitObjects.Any(o => o.Time <tp.Offset && o.EndTime> tp.Offset) where !tp.Uninherited select tp) { beatmapTo.BeatmapTiming.Remove(tp); } // Get timingpointschanges for every timingpoint from beatmapFrom that is in a sliderbody/spinnerbody for both beatmapTo and BeatmapFrom timingPointsChanges.AddRange(from ho in beatmapFrom.HitObjects from tp in ho.BodyHitsounds where beatmapTo.HitObjects.Any(o => o.Time <tp.Offset && o.EndTime> tp.Offset) select new TimingPointsChange(tp.Copy(), sampleset: arg.CopySampleSets, index: arg.CopySampleSets, volume: arg.CopyVolumes)); } // Apply the timingpoint changes TimingPointsChange.ApplyChanges(beatmapTo.BeatmapTiming, timingPointsChanges); processedTimeline = tlTo; } if (arg.CopyStoryboardedSamples) { if (arg.CopyMode == 0) { beatmapTo.StoryboardSoundSamples.Clear(); } beatmapTo.GiveObjectsGreenlines(); processedTimeline.GiveTimingPoints(beatmapTo.BeatmapTiming); var mapDir = editorTo.GetParentFolder(); var firstSamples = HitsoundImporter.AnalyzeSamples(mapDir, true); var samplesTo = new HashSet <StoryboardSoundSample>(beatmapTo.StoryboardSoundSamples); var mode = (GameMode)beatmapTo.General["Mode"].IntValue; foreach (var sampleFrom in beatmapFrom.StoryboardSoundSamples) { if (arg.IgnoreHitsoundSatisfiedSamples) { var tloHere = processedTimeline.TimelineObjects.FindAll(o => Math.Abs(o.Time - sampleFrom.StartTime) <= arg.TemporalLeniency); var samplesHere = new HashSet <string>(); foreach (var tlo in tloHere) { foreach (var filename in tlo.GetPlayingFilenames(mode)) { var samplePath = Path.Combine(mapDir, filename); var fullPathExtLess = Path.Combine(Path.GetDirectoryName(samplePath), Path.GetFileNameWithoutExtension(samplePath)); if (firstSamples.Keys.Contains(fullPathExtLess)) { samplePath = firstSamples[fullPathExtLess]; } samplesHere.Add(samplePath); } } var sbSamplePath = Path.Combine(mapDir, sampleFrom.FilePath); var sbFullPathExtLess = Path.Combine(Path.GetDirectoryName(sbSamplePath), Path.GetFileNameWithoutExtension(sbSamplePath)); if (firstSamples.Keys.Contains(sbFullPathExtLess)) { sbSamplePath = firstSamples[sbFullPathExtLess]; } if (samplesHere.Contains(sbSamplePath)) { continue; } } // Add the StoryboardSoundSamples from beatmapFrom to beatmapTo if it doesn't already have the sample if (!samplesTo.Contains(sampleFrom)) { beatmapTo.StoryboardSoundSamples.Add(sampleFrom); } } // Sort the storyboarded samples beatmapTo.StoryboardSoundSamples.Sort(); } if (arg.MuteSliderends) { var timingPointsChanges = new List <TimingPointsChange>(); beatmapTo.GiveObjectsGreenlines(); processedTimeline.GiveTimingPoints(beatmapTo.BeatmapTiming); foreach (var tloTo in processedTimeline.TimelineObjects) { if (FilterMuteTlo(tloTo, beatmapTo, arg)) { // Set volume to 5%, remove all hitsounds, apply customindex and sampleset tloTo.SampleSet = arg.MutedSampleSet; tloTo.AdditionSet = 0; tloTo.Normal = false; tloTo.Whistle = false; tloTo.Finish = false; tloTo.Clap = false; tloTo.HitsoundsToOrigin(); // Add timingpointschange to copy timingpoint hitsounds var tp = tloTo.HitsoundTimingPoint.Copy(); tp.Offset = tloTo.Time; tp.SampleSet = arg.MutedSampleSet; tp.SampleIndex = arg.MutedIndex; tp.Volume = 5; timingPointsChanges.Add(new TimingPointsChange(tp, sampleset: true, index: doMutedIndex, volume: true)); } else { // Add timingpointschange to preserve index and volume and sampleset var tp = tloTo.HitsoundTimingPoint.Copy(); tp.Offset = tloTo.Time; timingPointsChanges.Add(new TimingPointsChange(tp, sampleset: true, index: doMutedIndex, volume: true)); } } // Apply the timingpoint changes TimingPointsChange.ApplyChanges(beatmapTo.BeatmapTiming, timingPointsChanges); } // Save the file editorTo.SaveFile(); // Export the sample schema if there are samples if (sampleSchema.Count > 0) { string exportFolder = MainWindow.ExportPath; DirectoryInfo di = new DirectoryInfo(exportFolder); foreach (FileInfo file in di.GetFiles()) { file.Delete(); } HitsoundExporter.ExportSampleSchema(sampleSchema, exportFolder); System.Diagnostics.Process.Start(exportFolder); } // Update progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(++mapsDone * 100 / paths.Length); } } return("Done!"); }
public static void ExportHitsounds(List <HitsoundEvent> hitsounds, string baseBeatmap, string exportFolder, string exportMapName, GameMode exportGameMode, bool useGreenlines, bool useStoryboard) { var editor = EditorReaderStuff.GetNewestVersionOrNot(baseBeatmap); Beatmap beatmap = editor.Beatmap; if (useStoryboard) { beatmap.StoryboardSoundSamples.Clear(); foreach (var h in hitsounds.Where(h => !string.IsNullOrEmpty(h.Filename))) { beatmap.StoryboardSoundSamples.Add(new StoryboardSoundSample(h.Time, 0, h.Filename, h.Volume * 100)); } } else { // Make new timing points // Add red lines List <TimingPoint> timingPoints = beatmap.BeatmapTiming.GetAllRedlines(); List <TimingPointsChange> timingPointsChanges = timingPoints.Select(tp => new TimingPointsChange(tp, mpb: true, meter: true, inherited: true, omitFirstBarLine: true)) .ToList(); // Add hitsound stuff // Replace all hitobjects with the hitsounds beatmap.HitObjects.Clear(); foreach (HitsoundEvent h in hitsounds) { if (useGreenlines) { TimingPoint tp = beatmap.BeatmapTiming.GetTimingPointAtTime(h.Time + 5).Copy(); tp.Offset = h.Time; tp.SampleIndex = h.CustomIndex; h.CustomIndex = 0; // Set it to default value because it gets handled by greenlines now tp.Volume = Math.Round(tp.Volume * h.Volume); h.Volume = 0; // Set it to default value because it gets handled by greenlines now timingPointsChanges.Add(new TimingPointsChange(tp, index: true, volume: true)); } beatmap.HitObjects.Add(new HitObject(h.Pos, h.Time, 5, h.GetHitsounds(), h.SampleSet, h.Additions, h.CustomIndex, h.Volume * 100, h.Filename)); } // Replace the old timingpoints beatmap.BeatmapTiming.TimingPoints.Clear(); TimingPointsChange.ApplyChanges(beatmap.BeatmapTiming, timingPointsChanges); } // Change version to hitsounds beatmap.General["StackLeniency"] = new TValue("0.0"); beatmap.General["Mode"] = new TValue(((int)exportGameMode).ToInvariant()); beatmap.Metadata["Version"] = new TValue(exportMapName); if (exportGameMode == GameMode.Mania) { // Count the number of distinct X positions int numXPositions = new HashSet <double>(hitsounds.Select(h => h.Pos.X)).Count; int numKeys = MathHelper.Clamp(numXPositions, 1, 18); beatmap.Difficulty["CircleSize"] = new TValue(numKeys.ToInvariant()); } else { beatmap.Difficulty["CircleSize"] = new TValue("4"); } // Save the file to the export folder editor.SaveFile(Path.Combine(exportFolder, beatmap.GetFileName())); }
private string Complete_Sliders(SliderCompletionatorVm arg, BackgroundWorker worker, DoWorkEventArgs _) { int slidersCompleted = 0; var reader = EditorReaderStuff.GetFullEditorReaderOrNot(out var editorReaderException1); if (arg.ImportModeSetting == SliderCompletionatorVm.ImportMode.Selected && editorReaderException1 != null) { throw new Exception("Could not fetch selected hit objects.", editorReaderException1); } foreach (string path in arg.Paths) { var editor = EditorReaderStuff.GetNewestVersionOrNot(path, reader, out var selected, out var editorReaderException2); if (arg.ImportModeSetting == SliderCompletionatorVm.ImportMode.Selected && editorReaderException2 != null) { throw new Exception("Could not fetch selected hit objects.", editorReaderException2); } Beatmap beatmap = editor.Beatmap; Timing timing = beatmap.BeatmapTiming; List <HitObject> markedObjects = arg.ImportModeSetting == SliderCompletionatorVm.ImportMode.Selected ? selected : arg.ImportModeSetting == SliderCompletionatorVm.ImportMode.Bookmarked ? beatmap.GetBookmarkedObjects() : arg.ImportModeSetting == SliderCompletionatorVm.ImportMode.Time ? beatmap.QueryTimeCode(arg.TimeCode).ToList() : beatmap.HitObjects; for (int i = 0; i < markedObjects.Count; i++) { HitObject ho = markedObjects[i]; if (ho.IsSlider) { double oldSpatialLength = ho.PixelLength; double newSpatialLength = arg.SpatialLength != -1 ? ho.GetSliderPath(fullLength: true).Distance *arg.SpatialLength : oldSpatialLength; double oldTemporalLength = timing.CalculateSliderTemporalLength(ho.Time, ho.PixelLength); double newTemporalLength = arg.TemporalLength != -1 ? timing.GetMpBAtTime(ho.Time) * arg.TemporalLength : oldTemporalLength; double oldSv = timing.GetSvAtTime(ho.Time); double newSv = oldSv / ((newSpatialLength / oldSpatialLength) / (newTemporalLength / oldTemporalLength)); if (double.IsNaN(newSv)) { throw new Exception("Encountered NaN slider velocity. Make sure none of the inputs are zero."); } ho.SliderVelocity = newSv; ho.PixelLength = newSpatialLength; // Scale anchors to completion if (arg.MoveAnchors) { ho.SetAllCurvePoints(SliderPathUtil.MoveAnchorsToLength( ho.GetAllCurvePoints(), ho.SliderType, ho.PixelLength, out var pathType)); ho.SliderType = pathType; } slidersCompleted++; } if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(i / markedObjects.Count); } } // Reconstruct SliderVelocity List <TimingPointsChange> timingPointsChanges = new List <TimingPointsChange>(); // Add Hitobject stuff foreach (HitObject ho in beatmap.HitObjects) { if (ho.IsSlider) // SliderVelocity changes { TimingPoint tp = ho.TimingPoint.Copy(); tp.Offset = ho.Time; tp.MpB = ho.SliderVelocity; timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true)); } } // Add the new SliderVelocity changes TimingPointsChange.ApplyChanges(timing, timingPointsChanges); // Save the file editor.SaveFile(); } // Complete progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(100); } // Do stuff if (arg.Quick) { RunFinished?.Invoke(this, new RunToolCompletedEventArgs(true, reader != null)); } // Make an accurate message string message = ""; if (Math.Abs(slidersCompleted) == 1) { message += "Successfully completed " + slidersCompleted + " slider!"; } else { message += "Successfully completed " + slidersCompleted + " sliders!"; } return(arg.Quick ? "" : message); }
/// <summary> /// Places each hit object of the pattern beatmap into the other beatmap and applies timingpoint changes to copy timingpoint stuff aswell. /// The given pattern beatmap could be modified by this method if protectBeatmapPattern is false. /// </summary> /// <param name="patternBeatmap">The pattern beatmap to be placed into the beatmap.</param> /// <param name="beatmap">To beatmap to place the pattern in.</param> /// <param name="offset">An offset to move the pattern beatmap in time with.</param> /// <param name="protectBeatmapPattern">If true, copies the pattern beatmap to prevent the pattern beatmap from being modified by this method.</param> public void PlaceOsuPattern(Beatmap patternBeatmap, Beatmap beatmap, double offset = 0, bool protectBeatmapPattern = true) { if (protectBeatmapPattern) { // Copy so the original pattern doesnt get changed patternBeatmap = patternBeatmap.DeepCopy(); } // Do the offset if (Math.Abs(offset) > Precision.DOUBLE_EPSILON) { patternBeatmap.OffsetTime(offset); } // We adjust the pattern first so it alligns with the beatmap. // The right timing is applied and optional pre-processing is applied. // Sliderends and object timingpoints get recalculated. PreparePattern(patternBeatmap, beatmap, out var parts, out var timingPointsChanges); // Keep just the timing point changes which are inside the parts. // These timing point changes have everything that is necessary for inside the parts of the pattern. (even timing) timingPointsChanges = timingPointsChanges.Where(tpc => parts.Any(part => part.StartTime <= tpc.MyTP.Offset && part.EndTime >= tpc.MyTP.Offset)).ToList(); // Remove stuff if (PatternOverwriteMode != PatternOverwriteMode.NoOverwrite) { foreach (var part in parts) { RemovePartOfBeatmap(beatmap, part.StartTime - Padding, part.EndTime + Padding); } } // Add timingpoint changes for each hitobject to make sure they still have the wanted SV and hitsounds (especially near the edges of parts) // It is possible for the timingpoint of a hitobject at the start of a part to be outside of the part, so this fixes issues related to that timingPointsChanges.AddRange( beatmap.HitObjects.Where(ho => ho.TimingPoint != null) .Select(GetSvChange)); if (IncludeHitsounds) { timingPointsChanges.AddRange( beatmap.HitObjects.Where(ho => ho.HitsoundTimingPoint != null) .Select(GetHitsoundChange)); } // Apply the changes TimingPointsChange.ApplyChanges(beatmap.BeatmapTiming, timingPointsChanges); // Add the hitobjects of the pattern beatmap.HitObjects.AddRange(patternBeatmap.HitObjects); // Sort hitobjects beatmap.SortHitObjects(); if (FixColourHax) { beatmap.FixComboSkip(); } beatmap.GiveObjectsGreenlines(); beatmap.CalculateSliderEndTimes(); }
/// <summary> /// Does a procedure similar to <see cref="MapCleaner"/> which adjusts the pattern so it fits in the beatmap. /// It does so according to the options selected in this. /// </summary> /// <param name="patternBeatmap"></param> /// <param name="beatmap"></param> /// <param name="parts"></param> /// <param name="timingPointsChanges"></param> private void PreparePattern(Beatmap patternBeatmap, Beatmap beatmap, out List <Part> parts, out List <TimingPointsChange> timingPointsChanges) { double patternStartTime = patternBeatmap.GetHitObjectStartTime(); Timing originalTiming = beatmap.BeatmapTiming; Timing patternTiming = patternBeatmap.BeatmapTiming; GameMode targetMode = (GameMode)beatmap.General["Mode"].IntValue; double originalCircleSize = beatmap.Difficulty["CircleSize"].DoubleValue; double patternCircleSize = patternBeatmap.Difficulty["CircleSize"].DoubleValue; double originalTickRate = beatmap.Difficulty["SliderTickRate"].DoubleValue; double patternTickRate = patternBeatmap.Difficulty["SliderTickRate"].DoubleValue; // Don't include SV changes if it is based on nothing bool includePatternSliderVelocity = patternTiming.Count > 0; // Avoid including hitsounds if there are no timingpoints to get hitsounds from bool includeTimingPointHitsounds = IncludeHitsounds && patternTiming.Count > 0; // Don't scale to new timing if the pattern has no timing to speak of bool scaleToNewTiming = ScaleToNewTiming && patternTiming.Redlines.Count > 0; // Avoid overwriting timing if the pattern has no redlines TimingOverwriteMode timingOverwriteMode = patternTiming.Redlines.Count > 0 ? TimingOverwriteMode : TimingOverwriteMode.OriginalTimingOnly; // Get the scale for custom scale x CS scale double csScale = Beatmap.GetHitObjectRadius(originalCircleSize) / Beatmap.GetHitObjectRadius(patternCircleSize); double spatialScale = ScaleToNewCircleSize && !double.IsNaN(csScale) ? CustomScale * csScale : CustomScale; // Get a BPM multiplier to fix the tick rate // This multiplier is not meant to change SV so this is subtracted from the greenline SV later double bpmMultiplier = FixTickRate ? patternTickRate / originalTickRate : 1; // Dont give new combo to all hit objects which were actually new combo in the pattern, // because it leads to unexpected NC's at the start of patterns. // Collect Kiai toggles List <TimingPoint> kiaiToggles = new List <TimingPoint>(); bool lastKiai = false; // If not including the kiai of the pattern, add the kiai of the original map. // This has to be done because this part of the original map might get deleted. foreach (TimingPoint tp in IncludeKiai ? patternTiming.TimingPoints : originalTiming.TimingPoints) { if (tp.Kiai != lastKiai || kiaiToggles.Count == 0) { kiaiToggles.Add(tp.Copy()); lastKiai = tp.Kiai; } } // Collect SliderVelocity changes for mania/taiko List <TimingPoint> svChanges = new List <TimingPoint>(); double lastSV = -100; // If not including the SV of the pattern, add the SV of the original map. // This has to be done because this part of the original map might get deleted. foreach (TimingPoint tp in includePatternSliderVelocity ? patternTiming.TimingPoints : originalTiming.TimingPoints) { if (tp.Uninherited) { lastSV = -100; } else { if (Math.Abs(tp.MpB - lastSV) > Precision.DOUBLE_EPSILON) { svChanges.Add(tp.Copy()); lastSV = tp.MpB; } } } // If not including the SV of the pattern, set the SV of sliders to that of the original beatmap, // so the pattern will take over the SV of the original beatmap. if (!includePatternSliderVelocity) { foreach (var ho in patternBeatmap.HitObjects.Where(ho => ho.IsSlider)) { ho.SliderVelocity = originalTiming.GetSvAtTime(ho.Time); } } // Get the timeline before moving all objects so it has the correct hitsounds // Make sure that moving the objects in the pattern moves the timeline objects aswell // This method is NOT safe to use in beat time Timeline patternTimeline = patternBeatmap.GetTimeline(); Timing transformOriginalTiming = originalTiming; Timing transformPatternTiming = patternTiming; if (scaleToNewTiming) { // Transform everything to beat time relative to pattern start time foreach (var ho in patternBeatmap.HitObjects) { double oldEndTime = ho.GetEndTime(false); ho.Time = patternTiming.GetBeatLength(patternStartTime, ho.Time); ho.EndTime = patternTiming.GetBeatLength(patternStartTime, oldEndTime); // The body hitsounds are not copies of timingpoints in patternTiming so they should be copied before changing offset for (int i = 0; i < ho.BodyHitsounds.Count; i++) { TimingPoint tp = ho.BodyHitsounds[i].Copy(); tp.Offset = patternTiming.GetBeatLength(patternStartTime, tp.Offset); ho.BodyHitsounds[i] = tp; } } foreach (var tp in kiaiToggles.Concat(svChanges)) { tp.Offset = patternTiming.GetBeatLength(patternStartTime, tp.Offset); } // Transform the pattern redlines to beat time // This will not change the order of redlines (unless negative BPM exists) transformPatternTiming = patternTiming.Copy(); foreach (var tp in transformPatternTiming.Redlines) { tp.Offset = patternTiming.GetBeatLength(patternStartTime, tp.Offset); } // Transform the original timingpoints to beat time // This will not change the order of timingpoints (unless negative BPM exists) transformOriginalTiming = originalTiming.Copy(); foreach (var tp in transformOriginalTiming.TimingPoints) { tp.Offset = originalTiming.GetBeatLength(patternStartTime, tp.Offset); } } // Fix SV for the new global SV var globalSvFactor = transformOriginalTiming.SliderMultiplier / transformPatternTiming.SliderMultiplier; if (FixGlobalSv) { foreach (HitObject ho in patternBeatmap.HitObjects.Where(o => o.IsSlider)) { ho.SliderVelocity *= globalSvFactor; } foreach (TimingPoint tp in svChanges) { tp.MpB *= globalSvFactor; } } else { foreach (HitObject ho in patternBeatmap.HitObjects.Where(o => o.IsSlider)) { ho.TemporalLength /= globalSvFactor; } } // Partition the pattern based on the timing in the pattern if (PatternOverwriteMode == PatternOverwriteMode.PartitionedOverwrite) { parts = PartitionBeatmap(patternBeatmap, scaleToNewTiming); } else { parts = new List <Part> { new Part(patternBeatmap.HitObjects[0].Time, patternBeatmap.HitObjects[patternBeatmap.HitObjects.Count - 1].Time, patternBeatmap.HitObjects) }; } // Construct a new timing which is a mix of the beatmap and the pattern. // If scaleToNewTiming then use beat relative values to determine the duration of timing sections in the pattern. // scaleToNewTiming must scale all the partitions, timingpoints, hitobjects, and events (if applicable). Timing newTiming = new Timing(transformOriginalTiming.SliderMultiplier); var lastEndTime = double.NegativeInfinity; foreach (var part in parts) { var startTime = part.StartTime; var endTime = part.EndTime; // Subtract one to omit BPM changes right on the end of the part. // Add the redlines in between patterns newTiming.AddRange(transformOriginalTiming.GetRedlinesInRange(lastEndTime, startTime, false)); var startOriginalRedline = transformOriginalTiming.GetRedlineAtTime(startTime); // Minus 1 the offset so its possible to have a custom BPM redline right on the start time if you have // the default BPM redline before it. var patternDefaultMpb = transformPatternTiming.GetMpBAtTime(startTime - 2 * Precision.DOUBLE_EPSILON); TimingPoint[] inPartRedlines; TimingPoint startPartRedline; switch (timingOverwriteMode) { case TimingOverwriteMode.PatternTimingOnly: // Subtract one from the end time to omit BPM changes right on the end of the part. inPartRedlines = transformPatternTiming.GetRedlinesInRange(startTime, Math.Max(startTime, endTime - 2 * Precision.DOUBLE_EPSILON)).ToArray(); startPartRedline = transformPatternTiming.GetRedlineAtTime(startTime); break; case TimingOverwriteMode.InPatternAbsoluteTiming: var tempInPartRedlines = transformPatternTiming.GetRedlinesInRange(startTime, endTime - 2 * Precision.DOUBLE_EPSILON); // Replace all parts in the pattern which have the default BPM to timing from the target beatmap. inPartRedlines = tempInPartRedlines.Select(tp => { if (Precision.AlmostEquals(tp.MpB, patternDefaultMpb)) { var tp2 = transformOriginalTiming.GetRedlineAtTime(tp.Offset).Copy(); tp2.Offset = tp2.Offset; return(tp2); } return(tp); }).ToArray(); startPartRedline = startOriginalRedline; break; case TimingOverwriteMode.InPatternRelativeTiming: // Multiply mix the pattern timing and the original timing together. // The pattern timing divided by the default BPM will be used as a scalar for the original timing. var tempInPartRedlines2 = transformPatternTiming.GetRedlinesInRange(startTime, endTime - 2 * Precision.DOUBLE_EPSILON); var tempInOriginalRedlines = transformOriginalTiming.GetRedlinesInRange(startTime, endTime - 2 * Precision.DOUBLE_EPSILON); // Replace all parts in the pattern which have the default BPM to timing from the target beatmap. inPartRedlines = tempInPartRedlines2.Select(tp => { var tp2 = tp.Copy(); tp2.MpB *= transformOriginalTiming.GetMpBAtTime(tp.Offset) / patternDefaultMpb; return(tp2); }).Concat(tempInOriginalRedlines.Select(tp => { var tp2 = tp.Copy(); tp2.MpB *= transformPatternTiming.GetMpBAtTime(tp.Offset) / patternDefaultMpb; return(tp2); })).ToArray(); startPartRedline = transformPatternTiming.GetRedlineAtTime(startTime).Copy(); startPartRedline.MpB *= transformOriginalTiming.GetMpBAtTime(startTime) / patternDefaultMpb; break; default: // Original timing only // Subtract one from the end time to omit BPM changes right on the end of the part. inPartRedlines = transformOriginalTiming.GetRedlinesInRange(startTime, Math.Max(startTime, endTime - 2 * Precision.DOUBLE_EPSILON)).ToArray(); startPartRedline = transformOriginalTiming.GetRedlineAtTime(startTime); break; } // Add the redlines for inside the part newTiming.AddRange(inPartRedlines); // If the pattern starts with different BPM than the map add an extra redline at the start of the pattern // to make sure it the pattern starts out at the right BPM as we only copy the timingpoints during the pattern itself // and the redline may be way before that. // This will probably only do something on the PatternTimingOnly mode as the other modes make sure // the BPM at the start of the pattern will be the same as the original beatmap anyways. if (Math.Abs(startPartRedline.MpB * bpmMultiplier - startOriginalRedline.MpB) > Precision.DOUBLE_EPSILON) { // We dont have to add the redline again if its already during the pattern. if (Math.Abs(startPartRedline.Offset - startTime) > Precision.DOUBLE_EPSILON) { var copy = startPartRedline.Copy(); copy.Offset = startTime; newTiming.Add(copy); } } // Fix SV for the new BPM, so the SV effect of the new BPM is cancelled if (FixBpmSv) { if (scaleToNewTiming) { foreach (HitObject ho in patternBeatmap.HitObjects.Where(o => o.IsSlider)) { var bpmSvFactor = SnapToNewTiming ? transformPatternTiming.GetMpBAtTime(ho.Time) / newTiming.GetMpBAtTime(newTiming.ResnapBeatTime(ho.Time, BeatDivisors)) : transformPatternTiming.GetMpBAtTime(ho.Time) / newTiming.GetMpBAtTime(ho.Time); ho.SliderVelocity *= bpmSvFactor; } } else { foreach (HitObject ho in patternBeatmap.HitObjects.Where(o => o.IsSlider)) { var bpmSvFactor = SnapToNewTiming ? transformPatternTiming.GetMpBAtTime(ho.Time) / newTiming.GetMpBAtTime(newTiming.Resnap(ho.Time, BeatDivisors)) : transformPatternTiming.GetMpBAtTime(ho.Time) / newTiming.GetMpBAtTime(ho.Time); ho.SliderVelocity *= bpmSvFactor; } } } // Recalculate temporal length and re-assign redline for the sliderend resnapping later foreach (var ho in part.HitObjects) { ho.UnInheritedTimingPoint = newTiming.GetRedlineAtTime(ho.Time); if (ho.IsSlider) { // If scaleToNewTiming then the end time is already at the correct beat time // The SV has to be adjusted so the sliderend is really on the end time if (scaleToNewTiming) { var wantedMsDuration = (newTiming.GetMilliseconds(ho.GetEndTime(false), patternStartTime) - newTiming.GetMilliseconds(ho.Time, patternStartTime)) / ho.Repeat; var trueMsDuration = newTiming.CalculateSliderTemporalLength(SnapToNewTiming ? newTiming.ResnapBeatTime(ho.Time, BeatDivisors) : ho.Time, ho.PixelLength, ho.SliderVelocity); ho.SliderVelocity /= trueMsDuration / wantedMsDuration; } else { ho.TemporalLength = newTiming.CalculateSliderTemporalLength(SnapToNewTiming ? newTiming.Resnap(ho.Time, BeatDivisors) : ho.Time, ho.PixelLength, ho.SliderVelocity); } } } // Update the end time because the lengths of sliders changed endTime = part.HitObjects.Max(o => o.GetEndTime(!scaleToNewTiming)); part.EndTime = endTime; // Add a redline at the end of the pattern to make sure the BPM goes back to normal after the pattern. var endOriginalRedline = transformOriginalTiming.GetRedlineAtTime(endTime); var endPartRedline = inPartRedlines.LastOrDefault() ?? startPartRedline; if (Math.Abs(endPartRedline.MpB * bpmMultiplier - endOriginalRedline.MpB) > Precision.DOUBLE_EPSILON) { // We dont have to add the redline again if its already during the parts in between parts. if (Math.Abs(endOriginalRedline.Offset - endTime) > Precision.DOUBLE_EPSILON) { var copy = endOriginalRedline.Copy(); copy.Offset = endTime; newTiming.Add(copy); } } lastEndTime = endTime; } // Transform the beat time back to millisecond time Timing transformNewTiming = newTiming; if (scaleToNewTiming) { // Transform back the timing transformNewTiming = newTiming.Copy(); foreach (var tp in transformNewTiming.TimingPoints) { tp.Offset = Math.Floor(newTiming.GetMilliseconds(tp.Offset, patternStartTime) + Precision.DOUBLE_EPSILON); } // Transform back the parts foreach (Part part in parts) { part.StartTime = Math.Floor(newTiming.GetMilliseconds(part.StartTime, patternStartTime)); part.EndTime = Math.Floor(newTiming.GetMilliseconds(part.EndTime, patternStartTime)); } // Transform everything to millisecond time relative to pattern start time foreach (var ho in patternBeatmap.HitObjects) { // Calculate the millisecond end time before changing the start time because the end time getter uses the beat time start time var msEndTime = newTiming.GetMilliseconds(ho.GetEndTime(false), patternStartTime); ho.Time = newTiming.GetMilliseconds(ho.Time, patternStartTime); // End time has to be set after the time because the end time setter uses the millisecond start time ho.EndTime = msEndTime; foreach (var tp in ho.BodyHitsounds) { tp.Offset = newTiming.GetMilliseconds(tp.Offset, patternStartTime); } // It is necessary to resnap early so it can recalculate the duration using the right offset if (SnapToNewTiming) { ho.ResnapSelf(transformNewTiming, BeatDivisors); } if (ho.IsSlider) { ho.CalculateSliderTemporalLength(transformNewTiming, true); } ho.UnInheritedTimingPoint = transformNewTiming.GetRedlineAtTime(ho.Time); ho.UpdateTimelineObjectTimes(); } foreach (var tp in kiaiToggles.Concat(svChanges)) { tp.Offset = Math.Floor(newTiming.GetMilliseconds(tp.Offset, patternStartTime)); } } // Apply custom scale and rotate if (Math.Abs(spatialScale - 1) > Precision.DOUBLE_EPSILON || Math.Abs(CustomRotate) > Precision.DOUBLE_EPSILON) { // Create a transformation matrix for the custom scale and rotate // The rotation is inverted because the default osu! rotation goes clockwise Matrix2 transform = Matrix2.Mult(Matrix2.CreateScale(spatialScale), Matrix2.CreateRotation(-CustomRotate)); Vector2 centre = new Vector2(256, 192); foreach (var ho in patternBeatmap.HitObjects) { ho.Move(-centre); ho.Transform(transform); ho.Move(centre); // Scale pixel length and SV for sliders aswell if (ho.IsSlider) { ho.PixelLength *= spatialScale; ho.SliderVelocity /= spatialScale; } } // osu! clips coordinates to the bounds (0,512), so there is some space downwards to still place the pattern // Calculate the new bounds of the pattern and try to place it in the playfield var minX = patternBeatmap.HitObjects.Min(o => o.Pos.X); var minY = patternBeatmap.HitObjects.Min(o => o.Pos.Y); Vector2 offset = new Vector2(Math.Max(-minX, 0), Math.Max(-minY, 0)); if (offset.LengthSquared > 0) { foreach (var ho in patternBeatmap.HitObjects) { ho.Move(offset); } } } // Manualify stacks if (FixStackLeniency) { // If scale to new timing was used update the circle size of the pattern, // so it calculates stacks at the new size of the pattern. if (ScaleToNewCircleSize) { patternBeatmap.Difficulty["CircleSize"].DoubleValue = originalCircleSize; } patternBeatmap.CalculateEndPositions(); patternBeatmap.UpdateStacking(rounded: true); // Manualify by setting the base position to the stacked position foreach (var ho in patternBeatmap.HitObjects) { var offset = ho.StackedPos - ho.Pos; ho.Move(offset); } } // Resnap everything to the new timing. if (SnapToNewTiming) { // Resnap all objects foreach (HitObject ho in patternBeatmap.HitObjects) { ho.ResnapSelf(transformNewTiming, BeatDivisors); ho.ResnapEnd(transformNewTiming, BeatDivisors); ho.ResnapPosition(targetMode, patternCircleSize); // Resnap to column X positions for mania only } // Resnap Kiai toggles foreach (TimingPoint tp in kiaiToggles) { tp.ResnapSelf(transformNewTiming, BeatDivisors); } // Resnap SliderVelocity changes foreach (TimingPoint tp in svChanges) { tp.ResnapSelf(transformNewTiming, BeatDivisors); } } // Multiply BPM and divide SV foreach (var part in parts) { foreach (var tp in transformNewTiming.GetRedlinesInRange(part.StartTime - 2 * Precision.DOUBLE_EPSILON, part.EndTime, false)) { tp.MpB /= bpmMultiplier; // MpB is the inverse of the BPM } foreach (var ho in part.HitObjects) { ho.SliderVelocity *= bpmMultiplier; // SliderVelocity is the inverse of the multiplier } } // Make new timingpoints changes for the hitsounds and other stuff // Add redlines timingPointsChanges = transformNewTiming.Redlines.Select(tp => new TimingPointsChange(tp, mpb: true, meter: true, unInherited: true, omitFirstBarLine: true, fuzzyness: Precision.DOUBLE_EPSILON)).ToList(); // Add SliderVelocity changes for taiko and mania if (includePatternSliderVelocity && (targetMode == GameMode.Taiko || targetMode == GameMode.Mania)) { timingPointsChanges.AddRange(svChanges.Select(tp => new TimingPointsChange(tp, mpb: true, fuzzyness: 0.4))); } // Add Kiai toggles timingPointsChanges.AddRange(kiaiToggles.Select(tp => new TimingPointsChange(tp, kiai: true))); // Add Hitobject stuff foreach (HitObject ho in patternBeatmap.HitObjects) { if (ho.IsSlider) // SliderVelocity changes { TimingPoint tp = ho.TimingPoint.Copy(); tp.Offset = ho.Time; tp.MpB = ho.SliderVelocity; timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true, fuzzyness: 0.4)); } if (!IncludeHitsounds) { // Remove hitsounds and skip adding body hitsounds ho.ResetHitsounds(); continue; } if (includeTimingPointHitsounds) { // Body hitsounds bool vol = ho.IsSlider || ho.IsSpinner; bool sam = ho.IsSlider && ho.SampleSet == 0; bool ind = ho.IsSlider; timingPointsChanges.AddRange(ho.BodyHitsounds.Select(tp => new TimingPointsChange(tp, volume: vol, index: ind, sampleset: sam))); } } // Add timeline hitsounds if (includeTimingPointHitsounds) { foreach (TimelineObject tlo in patternTimeline.TimelineObjects) { if (tlo.HasHitsound) { // Add greenlines for hitsounds TimingPoint tp = tlo.HitsoundTimingPoint.Copy(); tp.Offset = tlo.Time; timingPointsChanges.Add(new TimingPointsChange(tp, sampleset: true, volume: true, index: true)); } } } // Replace the old timingpoints patternTiming.Clear(); TimingPointsChange.ApplyChanges(patternTiming, timingPointsChanges); patternBeatmap.GiveObjectsGreenlines(); patternBeatmap.CalculateSliderEndTimes(); }
private string Sliderate(SlideratorVm arg, BackgroundWorker worker) { // Make a position function for Sliderator Classes.Tools.Sliderator.PositionFunctionDelegate positionFunction; // Test if the function is a constant velocity bool constantVelocity; // We convert the graph GetValue function to a function that works like ms -> px // d is a value representing the number of milliseconds into the slider if (arg.GraphModeSetting == SlideratorVm.GraphMode.Velocity) { // Here we use SvGraphMultiplier to get an accurate conversion from SV to slider completion per beat // Completion = (100 * SliderMultiplier / PixelLength) * SV * Beats positionFunction = d => arg.GraphState.GetIntegral(0, d * arg.BeatsPerMinute / 60000) * arg.SvGraphMultiplier * arg.PixelLength; constantVelocity = Precision.AlmostEquals(AnchorCollection.GetMaxValue(arg.GraphState.Anchors), AnchorCollection.GetMinValue(arg.GraphState.Anchors)); } else { positionFunction = d => arg.GraphState.GetValue(d * arg.BeatsPerMinute / 60000) * arg.PixelLength; constantVelocity = Precision.AlmostEquals(AnchorCollection.GetMaxDerivative(arg.GraphState.Anchors), AnchorCollection.GetMinDerivative(arg.GraphState.Anchors)); } // Dont do Sliderator if the velocity is constant AND equal to the new velocity var simplifyShape = constantVelocity && Precision.AlmostEquals( arg.PixelLength / arg.GraphBeats / arg.GlobalSv / 100, arg.NewVelocity); // Get the highest velocity occuring in the graph double velocity = arg.NewVelocity; // Velocity is in SV // Do bad stuff to the velocity to make sure its the same SV as after writing it to .osu code velocity = -100 / double.Parse((-100 / velocity).ToInvariant(), CultureInfo.InvariantCulture); // Other velocity is in px / ms var otherVelocity = velocity * arg.SvGraphMultiplier * arg.PixelLength * arg.BeatsPerMinute / 60000; // Time between timeline ticks for stream export var deltaT = 60000 / arg.BeatsPerMinute / arg.BeatSnapDivisor; // Update progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(10); } List <Vector2> slideration = new List <Vector2>(); var sliderator = new Classes.Tools.Sliderator { PositionFunction = positionFunction, MaxT = arg.GraphBeats / arg.BeatsPerMinute * 60000, Velocity = otherVelocity, MinDendriteLength = arg.MinDendrite }; if (!simplifyShape) { // Get slider path like from the hit object preview var sliderPath = new SliderPath(arg.VisibleHitObject.SliderType, arg.VisibleHitObject.GetAllCurvePoints().ToArray(), GetMaxCompletion(arg, arg.GraphState.Anchors) * arg.PixelLength); var path = new List <Vector2>(); sliderPath.GetPathToProgress(path, 0, 1); // Update progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(20); } // Do Sliderator sliderator.SetPath(path); slideration = arg.ExportAsStream ? sliderator.SliderateStream(deltaT) : sliderator.Sliderate(); // Check for some illegal output if (double.IsInfinity(sliderator.MaxS) || double.IsNaN(sliderator.MaxS) || slideration.Any(v => double.IsNaN(v.X) || double.IsNaN(v.Y))) { return("Encountered unexpected values from Sliderator. Please check your input."); } } // Update progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(60); } // Exporting stuff BeatmapEditor editor; bool editorRead = false; if (arg.DoEditorRead) { editor = EditorReaderStuff.GetNewestVersionOrNot(arg.Path, out _, out var exception); if (exception == null) { editorRead = true; } arg.DoEditorRead = false; } else { editor = new BeatmapEditor(arg.Path); } var beatmap = editor.Beatmap; var timing = beatmap.BeatmapTiming; // Get hit object that might be present at the export time or make a new one var hitObjectHere = beatmap.HitObjects.FirstOrDefault(o => Math.Abs(arg.ExportTime - o.Time) < 5) ?? new HitObject(arg.ExportTime, 0, SampleSet.Auto, SampleSet.Auto); // Clone the hit object to not affect the already existing hit object instance with changes var clone = new HitObject(hitObjectHere.GetLine()) { IsCircle = arg.ExportAsStream, IsSpinner = false, IsHoldNote = false, IsSlider = !arg.ExportAsStream }; // Update progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(70); } if (!arg.ExportAsStream) { // Give the new hit object the sliderated anchors if (simplifyShape) { // The velocity is constant, so you can simplify to the original slider shape clone.SetAllCurvePoints(arg.VisibleHitObject.GetAllCurvePoints()); clone.SliderType = arg.VisibleHitObject.SliderType; } else { clone.SetAllCurvePoints(slideration); clone.SliderType = PathType.Bezier; } clone.PixelLength = sliderator.MaxS; clone.SliderVelocity = -100 / velocity; // Add hit object if (arg.ExportModeSetting == SlideratorVm.ExportMode.Add) { beatmap.HitObjects.Add(clone); } else { beatmap.HitObjects.Remove(hitObjectHere); beatmap.HitObjects.Add(clone); } // Add SV var timingPointsChanges = new List <TimingPointsChange>(); if (arg.DelegateToBpm) { var tpAfter = timing.GetRedlineAtTime(clone.Time).Copy(); var tpOn = tpAfter.Copy(); tpAfter.Offset = clone.Time; tpOn.Offset = clone.Time - 1; // This one will be on the slider tpAfter.OmitFirstBarLine = true; tpOn.OmitFirstBarLine = true; // Express velocity in BPM tpOn.MpB /= -100 / clone.SliderVelocity; // NaN SV results in removal of slider ticks clone.SliderVelocity = arg.RemoveSliderTicks ? double.NaN : -100; // Add redlines timingPointsChanges.Add(new TimingPointsChange(tpOn, mpb: true, inherited: true, omitFirstBarLine: true, fuzzyness: 0)); timingPointsChanges.Add(new TimingPointsChange(tpAfter, mpb: true, inherited: true, omitFirstBarLine: true, fuzzyness: 0)); clone.Time -= 1; } // Add SV for every hit object so the SV doesnt change for anything else than the sliderated slider timingPointsChanges.AddRange(beatmap.HitObjects.Select(ho => { var sv = ho == clone ? ho.SliderVelocity : timing.GetSvAtTime(ho.Time); var tp = timing.GetTimingPointAtTime(ho.Time).Copy(); tp.MpB = sv; tp.Offset = ho.Time; return(new TimingPointsChange(tp, mpb: true, fuzzyness: 0)); })); TimingPointsChange.ApplyChanges(timing, timingPointsChanges); } else { // Add hit objects if (arg.ExportModeSetting == SlideratorVm.ExportMode.Override) { beatmap.HitObjects.Remove(hitObjectHere); } double t = arg.ExportTime; foreach (var pos in slideration) { clone.Pos = pos; clone.Time = t; beatmap.HitObjects.Add(clone); clone = new HitObject(clone.GetLine()) { IsCircle = true, IsSpinner = false, IsHoldNote = false, IsSlider = false, NewCombo = false }; t += deltaT; } } // Update progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(80); } beatmap.SortHitObjects(); editor.SaveFile(); // Complete progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(100); } // Do stuff if (arg.Quick) { RunFinished?.Invoke(this, new RunToolCompletedEventArgs(true, editorRead)); } return(arg.Quick ? string.Empty : "Done!"); }
private string TransformProperties(PropertyTransformerVM vm, BackgroundWorker worker, DoWorkEventArgs _) { bool doFilterMatch = vm.MatchFilter != -1 && vm.EnableFilters; bool doFilterRange = (vm.MinTimeFilter != -1 || vm.MaxTimeFilter != -1) && vm.EnableFilters; double min = vm.MinTimeFilter == -1 ? double.NegativeInfinity : vm.MinTimeFilter; double max = vm.MaxTimeFilter == -1 ? double.PositiveInfinity : vm.MaxTimeFilter; bool editorRead = EditorReaderStuff.TryGetFullEditorReader(out var reader); foreach (string path in vm.MapPaths) { var editor = EditorReaderStuff.GetBeatmapEditor(path, reader, editorRead); Beatmap beatmap = editor.Beatmap; // Count all the total amount of things to loop through int loops = 0; int totalLoops = beatmap.BeatmapTiming.TimingPoints.Count; if (vm.HitObjectTimeMultiplier != 1 || vm.HitObjectTimeOffset != 0) { totalLoops += beatmap.HitObjects.Count; } if (vm.BookmarkTimeMultiplier != 1 || vm.BookmarkTimeOffset != 0) { totalLoops += beatmap.GetBookmarks().Count; } if (vm.SBSampleTimeMultiplier != 1 || vm.SBSampleTimeOffset != 0) { totalLoops += beatmap.StoryboardSoundSamples.Count; } List <TimingPointsChange> timingPointsChanges = new List <TimingPointsChange>(); foreach (TimingPoint tp in beatmap.BeatmapTiming.TimingPoints) { // Offset if (vm.TimingpointOffsetMultiplier != 1 || vm.TimingpointOffsetOffset != 0) { if (Filter(tp.Offset, tp.Offset, doFilterMatch, doFilterRange, vm.MatchFilter, min, max)) { tp.Offset = Math.Round(tp.Offset * vm.TimingpointOffsetMultiplier + vm.TimingpointOffsetOffset); } } // BPM if (vm.TimingpointBPMMultiplier != 1 || vm.TimingpointBPMOffset != 0) { if (tp.Uninherited) { if (Filter(tp.GetBpm(), tp.Offset, doFilterMatch, doFilterRange, vm.MatchFilter, min, max)) { double newBPM = tp.GetBpm() * vm.TimingpointBPMMultiplier + vm.TimingpointBPMOffset; newBPM = vm.ClipProperties ? MathHelper.Clamp(newBPM, 15, 10000) : newBPM; // Clip the value if specified tp.MpB = 60000 / newBPM; } } } // Slider Velocity if (vm.TimingpointSVMultiplier != 1 || vm.TimingpointSVOffset != 0) { if (Filter(beatmap.BeatmapTiming.GetSvMultiplierAtTime(tp.Offset), tp.Offset, doFilterMatch, doFilterRange, vm.MatchFilter, min, max)) { TimingPoint tpchanger = tp.Copy(); double newSV = beatmap.BeatmapTiming.GetSvMultiplierAtTime(tp.Offset) * vm.TimingpointSVMultiplier + vm.TimingpointSVOffset; newSV = vm.ClipProperties ? MathHelper.Clamp(newSV, 0.1, 10) : newSV; // Clip the value if specified tpchanger.MpB = -100 / newSV; timingPointsChanges.Add(new TimingPointsChange(tpchanger, mpb: true)); } } // Index if (vm.TimingpointIndexMultiplier != 1 || vm.TimingpointIndexOffset != 0) { if (Filter(tp.SampleIndex, tp.Offset, doFilterMatch, doFilterRange, vm.MatchFilter, min, max)) { int newIndex = (int)Math.Round(tp.SampleIndex * vm.TimingpointIndexMultiplier + vm.TimingpointIndexOffset); tp.SampleIndex = vm.ClipProperties ? MathHelper.Clamp(newIndex, 0, 100) : newIndex; } } // Volume if (vm.TimingpointVolumeMultiplier != 1 || vm.TimingpointVolumeOffset != 0) { if (Filter(tp.Volume, tp.Offset, doFilterMatch, doFilterRange, vm.MatchFilter, min, max)) { int newVolume = (int)Math.Round(tp.Volume * vm.TimingpointVolumeMultiplier + vm.TimingpointVolumeOffset); tp.Volume = vm.ClipProperties ? MathHelper.Clamp(newVolume, 5, 100) : newVolume; } } // Update progress bar loops++; UpdateProgressBar(worker, loops * 100 / totalLoops); } // Hitobject Time if (vm.HitObjectTimeMultiplier != 1 || vm.HitObjectTimeOffset != 0) { foreach (HitObject ho in beatmap.HitObjects) { if (Filter(ho.Time, ho.Time, doFilterMatch, doFilterRange, vm.MatchFilter, min, max)) { ho.Time = Math.Round(ho.Time * vm.HitObjectTimeMultiplier + vm.HitObjectTimeOffset); } // Update progress bar loops++; UpdateProgressBar(worker, loops * 100 / totalLoops); } } // Bookmark Time if (vm.BookmarkTimeMultiplier != 1 || vm.BookmarkTimeOffset != 0) { List <double> newBookmarks = new List <double>(); List <double> bookmarks = beatmap.GetBookmarks(); foreach (double bookmark in bookmarks) { if (Filter(bookmark, bookmark, doFilterMatch, doFilterRange, vm.MatchFilter, min, max)) { newBookmarks.Add(Math.Round(bookmark * vm.BookmarkTimeMultiplier + vm.BookmarkTimeOffset)); } else { newBookmarks.Add(bookmark); } // Update progress bar loops++; UpdateProgressBar(worker, loops * 100 / totalLoops); } beatmap.SetBookmarks(newBookmarks); } // Storyboarded sample Time if (vm.SBSampleTimeMultiplier != 1 || vm.SBSampleTimeOffset != 0) { foreach (StoryboardSoundSample ss in beatmap.StoryboardSoundSamples) { if (Filter(ss.Time, ss.Time, doFilterMatch, doFilterRange, vm.MatchFilter, min, max)) { ss.Time = Math.Round(ss.Time * vm.SBSampleTimeMultiplier + vm.SBSampleTimeOffset); } // Update progress bar loops++; UpdateProgressBar(worker, loops * 100 / totalLoops); } } TimingPointsChange.ApplyChanges(beatmap.BeatmapTiming, timingPointsChanges); // Save the file editor.SaveFile(); } return("Done!"); }
private string Complete_Sliders(SliderCompletionatorVm arg, BackgroundWorker worker, DoWorkEventArgs _) { int slidersCompleted = 0; var reader = EditorReaderStuff.GetFullEditorReaderOrNot(out var editorReaderException1); if (arg.ImportModeSetting == SliderCompletionatorVm.ImportMode.Selected && editorReaderException1 != null) { throw new Exception("Could not fetch selected hit objects.", editorReaderException1); } foreach (string path in arg.Paths) { var editor = EditorReaderStuff.GetNewestVersionOrNot(path, reader, out var selected, out var editorReaderException2); if (arg.ImportModeSetting == SliderCompletionatorVm.ImportMode.Selected && editorReaderException2 != null) { throw new Exception("Could not fetch selected hit objects.", editorReaderException2); } Beatmap beatmap = editor.Beatmap; Timing timing = beatmap.BeatmapTiming; List <HitObject> markedObjects = arg.ImportModeSetting switch { SliderCompletionatorVm.ImportMode.Selected => selected, SliderCompletionatorVm.ImportMode.Bookmarked => beatmap.GetBookmarkedObjects(), SliderCompletionatorVm.ImportMode.Time => beatmap.QueryTimeCode(arg.TimeCode).ToList(), SliderCompletionatorVm.ImportMode.Everything => beatmap.HitObjects, _ => throw new ArgumentException("Unexpected import mode.") }; for (int i = 0; i < markedObjects.Count; i++) { HitObject ho = markedObjects[i]; if (ho.IsSlider) { double mpb = timing.GetMpBAtTime(ho.Time); double oldDuration = timing.CalculateSliderTemporalLength(ho.Time, ho.PixelLength); double oldLength = ho.PixelLength; double oldSv = timing.GetSvAtTime(ho.Time); double newDuration = arg.UseEndTime ? arg.EndTime == -1 ? oldDuration : arg.EndTime - ho.Time : arg.Duration == -1 ? oldDuration : timing.WalkBeatsInMillisecondTime(arg.Duration, ho.Time) - ho.Time; double newLength = arg.Length == -1 ? oldLength : ho.GetSliderPath(fullLength: true).Distance *arg.Length; double newSv = arg.SliderVelocity == -1 ? oldSv : -100 / arg.SliderVelocity; switch (arg.FreeVariableSetting) { case SliderCompletionatorVm.FreeVariable.Velocity: newSv = -10000 * timing.SliderMultiplier * newDuration / (newLength * mpb); break; case SliderCompletionatorVm.FreeVariable.Duration: // This actually doesn't get used anymore because the .osu doesn't store the duration newDuration = newLength * newSv * mpb / (-10000 * timing.SliderMultiplier); break; case SliderCompletionatorVm.FreeVariable.Length: newLength = -10000 * timing.SliderMultiplier * newDuration / (newSv * mpb); break; default: throw new ArgumentException("Unexpected free variable setting."); } if (double.IsNaN(newSv)) { throw new Exception("Encountered NaN slider velocity. Make sure none of the inputs are zero."); } if (newDuration < 0) { throw new Exception("Encountered slider with negative duration. Make sure the end time is greater than the end time of all selected sliders."); } ho.SliderVelocity = newSv; ho.PixelLength = newLength; // Scale anchors to completion if (arg.MoveAnchors) { ho.SetAllCurvePoints(SliderPathUtil.MoveAnchorsToLength( ho.GetAllCurvePoints(), ho.SliderType, ho.PixelLength, out var pathType)); ho.SliderType = pathType; } slidersCompleted++; } if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(i / markedObjects.Count); } } // Reconstruct SliderVelocity List <TimingPointsChange> timingPointsChanges = new List <TimingPointsChange>(); // Add Hitobject stuff foreach (HitObject ho in beatmap.HitObjects) { // SliderVelocity changes if (ho.IsSlider) { if (markedObjects.Contains(ho) && arg.DelegateToBpm) { var tpAfter = timing.GetRedlineAtTime(ho.Time).Copy(); var tpOn = tpAfter.Copy(); tpAfter.Offset = ho.Time; tpOn.Offset = ho.Time - 1; // This one will be on the slider tpAfter.OmitFirstBarLine = true; tpOn.OmitFirstBarLine = true; // Express velocity in BPM tpOn.MpB *= ho.SliderVelocity / -100; // NaN SV results in removal of slider ticks ho.SliderVelocity = arg.RemoveSliderTicks ? double.NaN : -100; // Add redlines timingPointsChanges.Add(new TimingPointsChange(tpOn, mpb: true, unInherited: true, omitFirstBarLine: true, fuzzyness: Precision.DOUBLE_EPSILON)); timingPointsChanges.Add(new TimingPointsChange(tpAfter, mpb: true, unInherited: true, omitFirstBarLine: true, fuzzyness: Precision.DOUBLE_EPSILON)); ho.Time -= 1; } TimingPoint tp = ho.TimingPoint.Copy(); tp.Offset = ho.Time; tp.MpB = ho.SliderVelocity; timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true, fuzzyness: Precision.DOUBLE_EPSILON)); } } // Add the new SliderVelocity changes TimingPointsChange.ApplyChanges(timing, timingPointsChanges); // Save the file editor.SaveFile(); } // Complete progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(100); } // Do stuff RunFinished?.Invoke(this, new RunToolCompletedEventArgs(true, reader != null, arg.Quick)); // Make an accurate message string message = ""; if (Math.Abs(slidersCompleted) == 1) { message += "Successfully completed " + slidersCompleted + " slider!"; } else { message += "Successfully completed " + slidersCompleted + " sliders!"; } return(arg.Quick ? "" : message); }
private string Complete_Sliders(Arguments arg, BackgroundWorker worker, DoWorkEventArgs _) { int slidersCompleted = 0; bool editorRead = EditorReaderStuff.TryGetFullEditorReader(out var reader); foreach (string path in arg.Paths) { var editor = EditorReaderStuff.GetBeatmapEditor(path, reader, editorRead, out var selected, out var editorActuallyRead); if (arg.SelectionMode == 0 && !editorActuallyRead) { return(EditorReaderStuff.SelectedObjectsReadFailText); } Beatmap beatmap = editor.Beatmap; Timing timing = beatmap.BeatmapTiming; List <HitObject> markedObjects = arg.SelectionMode == 0 ? selected : arg.SelectionMode == 1 ? beatmap.GetBookmarkedObjects() : beatmap.HitObjects; for (int i = 0; i < markedObjects.Count; i++) { HitObject ho = markedObjects[i]; if (ho.IsSlider) { double oldSpatialLength = ho.PixelLength; double newSpatialLength = arg.SpatialLength != -1 ? ho.GetSliderPath(fullLength: true).Distance *arg.SpatialLength : oldSpatialLength; double oldTemporalLength = timing.CalculateSliderTemporalLength(ho.Time, ho.PixelLength); double newTemporalLength = arg.TemporalLength != -1 ? timing.GetMpBAtTime(ho.Time) * arg.TemporalLength : oldTemporalLength; double oldSv = timing.GetSvAtTime(ho.Time); double newSv = oldSv / ((newSpatialLength / oldSpatialLength) / (newTemporalLength / oldTemporalLength)); ho.SliderVelocity = newSv; ho.PixelLength = newSpatialLength; // Scale anchors to completion if (arg.MoveAnchors) { ho.SetAllCurvePoints(SliderPathUtil.MoveAnchorsToLength( ho.GetAllCurvePoints(), ho.SliderType, ho.PixelLength, out var pathType)); ho.SliderType = pathType; } slidersCompleted++; } if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(i / markedObjects.Count); } } // Reconstruct SliderVelocity List <TimingPointsChange> timingPointsChanges = new List <TimingPointsChange>(); // Add Hitobject stuff foreach (HitObject ho in beatmap.HitObjects) { if (ho.IsSlider) // SliderVelocity changes { TimingPoint tp = ho.TimingPoint.Copy(); tp.Offset = ho.Time; tp.MpB = ho.SliderVelocity; timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true)); } } // Add the new SliderVelocity changes TimingPointsChange.ApplyChanges(timing, timingPointsChanges); // Save the file editor.SaveFile(); } // Complete progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(100); } // Do stuff if (arg.Quick) { RunFinished?.Invoke(this, new RunToolCompletedEventArgs(true, editorRead)); } // Make an accurate message string message = ""; if (Math.Abs(slidersCompleted) == 1) { message += "Successfully completed " + slidersCompleted + " slider!"; } else { message += "Successfully completed " + slidersCompleted + " sliders!"; } return(arg.Quick ? "" : message); }
/// <summary> /// Places each hit object of the pattern beatmap into the other beatmap and applies timingpoint changes to copy timingpoint stuff aswell. /// The given pattern beatmap could be modified by this method if protectBeatmapPattern is false. /// </summary> /// <param name="patternBeatmap">The pattern beatmap to be placed into the beatmap.</param> /// <param name="beatmap">To beatmap to place the pattern in.</param> /// <param name="offset">An offset to move the pattern beatmap in time with.</param> /// <param name="protectBeatmapPattern">If true, copies the pattern beatmap to prevent the pattern beatmap from being modified by this method.</param> public void PlaceOsuPattern(Beatmap patternBeatmap, Beatmap beatmap, double offset = 0, bool protectBeatmapPattern = true) { if (protectBeatmapPattern) { // Copy so the original pattern doesnt get changed patternBeatmap = patternBeatmap.DeepCopy(); } if (offset != 0) { patternBeatmap.OffsetTime(offset); } // Do some kind of processing to fix timing etc // Set the global SV and BPM in the pattern beatmap so the object end times can be calculated for the partitioning patternBeatmap.BeatmapTiming.SliderMultiplier = beatmap.BeatmapTiming.SliderMultiplier; patternBeatmap.BeatmapTiming.TimingPoints.RemoveAll(tp => tp.Uninherited); patternBeatmap.BeatmapTiming.TimingPoints.AddRange(beatmap.BeatmapTiming.GetAllRedlines()); patternBeatmap.BeatmapTiming.Sort(); patternBeatmap.CalculateSliderEndTimes(); // Partition the pattern beatmap List <Tuple <double, double> > parts; if (PatternOverwriteMode == PatternOverwriteMode.PartitionedOverwrite) { parts = PartitionBeatmap(patternBeatmap); } else { parts = new List <Tuple <double, double> > { new Tuple <double, double>(patternBeatmap.GetHitObjectStartTime(), patternBeatmap.GetHitObjectEndTime()) }; } // Remove stuff if (PatternOverwriteMode != PatternOverwriteMode.NoOverwrite) { foreach (var part in parts) { RemovePartOfBeatmap(beatmap, part.Item1 - Padding, part.Item2 + Padding); } } // Add the hitobjects of the pattern beatmap.HitObjects.AddRange(patternBeatmap.HitObjects); // Add timingpoint changes for each timingpoint in a part in the pattern var timingPointsChanges = new List <TimingPointsChange>(); foreach (var part in parts) { timingPointsChanges.AddRange( patternBeatmap.BeatmapTiming.TimingPoints.Where(tp => tp.Offset >= part.Item1 - Padding && tp.Offset <= part.Item2 + Padding) .Select(tp => GetTimingPointsChange(tp, true, true))); } // Add timingpoint changes for each hitobject to make sure they still have the wanted SV and hitsounds (especially near the edges of parts) // It is possible for the timingpoint of a hitobject at the start of a part to be outside of the part, so this fixes issues related to that timingPointsChanges.AddRange( beatmap.HitObjects.Where(ho => ho.TimingPoint != null) .Select(ho => GetTimingPointsChange(ho, true, true))); // Apply the changes TimingPointsChange.ApplyChanges(beatmap.BeatmapTiming, timingPointsChanges); // Sort hitobjects later so the timingpoints changes from the new hitobjects have priority beatmap.SortHitObjects(); }
private string TransformProperties(PropertyTransformerVm vm, BackgroundWorker worker, DoWorkEventArgs _) { var reader = EditorReaderStuff.GetFullEditorReaderOrNot(); foreach (string path in vm.ExportPaths) { Editor editor; if (Path.GetExtension(path) == ".osb") { editor = new StoryboardEditor(path); } else { editor = EditorReaderStuff.GetNewestVersionOrNot(path, reader); } if (editor is BeatmapEditor beatmapEditor) { Beatmap beatmap = beatmapEditor.Beatmap; List <TimingPointsChange> timingPointsChanges = new List <TimingPointsChange>(); foreach (TimingPoint tp in beatmap.BeatmapTiming.TimingPoints) { // Offset if (vm.TimingpointOffsetMultiplier != 1 || vm.TimingpointOffsetOffset != 0) { if (Filter(tp.Offset, tp.Offset, vm)) { tp.Offset = Math.Round(tp.Offset * vm.TimingpointOffsetMultiplier + vm.TimingpointOffsetOffset); } } // BPM if (vm.TimingpointBPMMultiplier != 1 || vm.TimingpointBPMOffset != 0) { if (tp.Uninherited) { if (Filter(tp.GetBpm(), tp.Offset, vm)) { double newBPM = tp.GetBpm() * vm.TimingpointBPMMultiplier + vm.TimingpointBPMOffset; newBPM = vm.ClipProperties ? MathHelper.Clamp(newBPM, 15, 10000) : newBPM; // Clip the value if specified tp.MpB = 60000 / newBPM; } } } // Slider Velocity if (vm.TimingpointSVMultiplier != 1 || vm.TimingpointSVOffset != 0) { if (Filter(beatmap.BeatmapTiming.GetSvMultiplierAtTime(tp.Offset), tp.Offset, vm)) { TimingPoint tpchanger = tp.Copy(); double newSV = beatmap.BeatmapTiming.GetSvMultiplierAtTime(tp.Offset) * vm.TimingpointSVMultiplier + vm.TimingpointSVOffset; newSV = vm.ClipProperties ? MathHelper.Clamp(newSV, 0.1, 10) : newSV; // Clip the value if specified tpchanger.MpB = -100 / newSV; timingPointsChanges.Add(new TimingPointsChange(tpchanger, mpb: true)); } } // Index if (vm.TimingpointIndexMultiplier != 1 || vm.TimingpointIndexOffset != 0) { if (Filter(tp.SampleIndex, tp.Offset, vm)) { int newIndex = (int)Math.Round(tp.SampleIndex * vm.TimingpointIndexMultiplier + vm.TimingpointIndexOffset); tp.SampleIndex = vm.ClipProperties ? MathHelper.Clamp(newIndex, 0, int.MaxValue) : newIndex; } } // Volume if (vm.TimingpointVolumeMultiplier != 1 || vm.TimingpointVolumeOffset != 0) { if (Filter(tp.Volume, tp.Offset, vm)) { int newVolume = (int)Math.Round(tp.Volume * vm.TimingpointVolumeMultiplier + vm.TimingpointVolumeOffset); tp.Volume = vm.ClipProperties ? MathHelper.Clamp(newVolume, 5, 100) : newVolume; } } } UpdateProgressBar(worker, 20); // Hitobject time if (vm.HitObjectTimeMultiplier != 1 || vm.HitObjectTimeOffset != 0) { foreach (HitObject ho in beatmap.HitObjects) { // Get the end time early because the start time gets modified double oldEndTime = ho.GetEndTime(false); if (Filter(ho.Time, ho.Time, vm)) { ho.Time = Math.Round(ho.Time * vm.HitObjectTimeMultiplier + vm.HitObjectTimeOffset); } // Transform end time of hold notes and spinner if ((ho.IsHoldNote || ho.IsSpinner) && Filter(oldEndTime, oldEndTime, vm)) { ho.EndTime = Math.Round(oldEndTime * vm.HitObjectTimeMultiplier + vm.HitObjectTimeOffset); } } } UpdateProgressBar(worker, 30); // Bookmark time if (vm.BookmarkTimeMultiplier != 1 || vm.BookmarkTimeOffset != 0) { List <double> newBookmarks = new List <double>(); List <double> bookmarks = beatmap.GetBookmarks(); foreach (double bookmark in bookmarks) { if (Filter(bookmark, bookmark, vm)) { newBookmarks.Add( Math.Round(bookmark * vm.BookmarkTimeMultiplier + vm.BookmarkTimeOffset)); } else { newBookmarks.Add(bookmark); } } beatmap.SetBookmarks(newBookmarks); } UpdateProgressBar(worker, 40); // Storyboarded event time if (vm.SBEventTimeMultiplier != 1 || vm.SBEventTimeOffset != 0) { foreach (Event ev in beatmap.StoryboardLayerBackground.Concat(beatmap.StoryboardLayerFail) .Concat(beatmap.StoryboardLayerPass).Concat(beatmap.StoryboardLayerForeground) .Concat(beatmap.StoryboardLayerOverlay)) { TransformEventTime(ev, vm.SBEventTimeMultiplier, vm.SBEventTimeOffset, vm); } } UpdateProgressBar(worker, 50); // Storyboarded sample time if (vm.SBSampleTimeMultiplier != 1 || vm.SBSampleTimeOffset != 0) { foreach (StoryboardSoundSample ss in beatmap.StoryboardSoundSamples) { if (Filter(ss.StartTime, ss.StartTime, vm)) { ss.StartTime = (int)Math.Round(ss.StartTime * vm.SBSampleTimeMultiplier + vm.SBSampleTimeOffset); } } } UpdateProgressBar(worker, 60); // Break time if (vm.BreakTimeMultiplier != 1 || vm.BreakTimeOffset != 0) { foreach (Break br in beatmap.BreakPeriods) { if (Filter(br.StartTime, br.StartTime, vm)) { br.StartTime = (int)Math.Round(br.StartTime * vm.BreakTimeMultiplier + vm.BreakTimeOffset); } if (Filter(br.EndTime, br.EndTime, vm)) { br.EndTime = (int)Math.Round(br.EndTime * vm.BreakTimeMultiplier + vm.BreakTimeOffset); } } } UpdateProgressBar(worker, 70); // Video start time if (vm.VideoTimeMultiplier != 1 || vm.VideoTimeOffset != 0) { foreach (Event ev in beatmap.BackgroundAndVideoEvents) { if (ev is Video video) { if (Filter(video.StartTime, video.StartTime, vm)) { video.StartTime = (int)Math.Round(video.StartTime * vm.VideoTimeMultiplier + vm.VideoTimeOffset); } } } } UpdateProgressBar(worker, 80); // Preview point time if (vm.PreviewTimeMultiplier != 1 || vm.PreviewTimeOffset != 0) { if (beatmap.General.ContainsKey("PreviewTime") && beatmap.General["PreviewTime"].IntValue != -1) { var previewTime = beatmap.General["PreviewTime"].DoubleValue; if (Filter(previewTime, previewTime, vm)) { var newPreviewTime = Math.Round(previewTime * vm.PreviewTimeMultiplier + vm.PreviewTimeOffset); beatmap.General["PreviewTime"].SetDouble(newPreviewTime); } } } UpdateProgressBar(worker, 90); TimingPointsChange.ApplyChanges(beatmap.BeatmapTiming, timingPointsChanges); // Save the file beatmapEditor.SaveFile(); UpdateProgressBar(worker, 100); } else if (editor is StoryboardEditor storyboardEditor) { StoryBoard storyboard = storyboardEditor.StoryBoard; // Storyboarded event time if (vm.SBEventTimeMultiplier != 1 || vm.SBEventTimeOffset != 0) { foreach (Event ev in storyboard.StoryboardLayerBackground.Concat(storyboard.StoryboardLayerFail) .Concat(storyboard.StoryboardLayerPass).Concat(storyboard.StoryboardLayerForeground) .Concat(storyboard.StoryboardLayerOverlay)) { TransformEventTime(ev, vm.SBEventTimeMultiplier, vm.SBEventTimeOffset, vm); } } UpdateProgressBar(worker, 50); // Storyboarded sample time if (vm.SBSampleTimeMultiplier != 1 || vm.SBSampleTimeOffset != 0) { foreach (StoryboardSoundSample ss in storyboard.StoryboardSoundSamples) { if (Filter(ss.StartTime, ss.StartTime, vm)) { ss.StartTime = (int)Math.Round(ss.StartTime * vm.SBSampleTimeMultiplier + vm.SBSampleTimeOffset); } } } UpdateProgressBar(worker, 70); // Video start time if (vm.VideoTimeMultiplier != 1 || vm.VideoTimeOffset != 0) { foreach (Event ev in storyboard.BackgroundAndVideoEvents) { if (ev is Video video) { if (Filter(video.StartTime, video.StartTime, vm)) { video.StartTime = (int)Math.Round(video.StartTime * vm.VideoTimeMultiplier + vm.VideoTimeOffset); } } } } UpdateProgressBar(worker, 90); // Save the file storyboardEditor.SaveFile(); UpdateProgressBar(worker, 100); } } return("Done!"); }
/// <summary> /// Cleans a map. /// </summary> /// <param name="editor">The editor of the beatmap that is going to be cleaned.</param> /// <param name="args">The arguments for how to clean the beatmap.</param> /// <param name="worker">The BackgroundWorker for updating progress.</param> /// <returns>Number of resnapped objects.</returns> public static MapCleanerResult CleanMap(BeatmapEditor editor, MapCleanerArgs args, BackgroundWorker worker = null) { UpdateProgressBar(worker, 0); Beatmap beatmap = editor.Beatmap; Timing timing = beatmap.BeatmapTiming; GameMode mode = (GameMode)beatmap.General["Mode"].IntValue; double circleSize = beatmap.Difficulty["CircleSize"].DoubleValue; string mapDir = editor.GetParentFolder(); Dictionary <string, string> firstSamples = HitsoundImporter.AnalyzeSamples(mapDir); int objectsResnapped = 0; int samplesRemoved = 0; // Collect timeline objects before resnapping, so the timingpoints // are still valid and the tlo's get the correct hitsounds and offsets. // Resnapping of the hit objects will move the tlo's aswell Timeline timeline = beatmap.GetTimeline(); // Collect Kiai toggles and SliderVelocity changes for mania/taiko List <TimingPoint> kiaiToggles = new List <TimingPoint>(); List <TimingPoint> svChanges = new List <TimingPoint>(); bool lastKiai = false; double lastSV = -100; foreach (TimingPoint tp in timing.TimingPoints) { if (tp.Kiai != lastKiai) { kiaiToggles.Add(tp.Copy()); lastKiai = tp.Kiai; } if (tp.Uninherited) { lastSV = -100; } else { if (tp.MpB != lastSV) { svChanges.Add(tp.Copy()); lastSV = tp.MpB; } } } UpdateProgressBar(worker, 9); // Resnap shit if (args.ResnapObjects) { // Resnap all objects foreach (HitObject ho in beatmap.HitObjects) { bool resnapped = ho.ResnapSelf(timing, args.BeatDivisors); if (resnapped) { objectsResnapped += 1; } ho.ResnapEnd(timing, args.BeatDivisors); ho.ResnapPosition(mode, circleSize); } UpdateProgressBar(worker, 18); // Resnap Kiai toggles foreach (TimingPoint tp in kiaiToggles) { tp.ResnapSelf(timing, args.BeatDivisors); } UpdateProgressBar(worker, 27); // Resnap SliderVelocity changes foreach (TimingPoint tp in svChanges) { tp.ResnapSelf(timing, args.BeatDivisors); } UpdateProgressBar(worker, 36); } if (args.ResnapBookmarks) { // Resnap the bookmarks List <double> bookmarks = beatmap.GetBookmarks(); List <double> newBookmarks = bookmarks.Select(o => timing.Resnap(o, args.BeatDivisors)).ToList(); // Remove duplicate bookmarks newBookmarks = newBookmarks.Distinct().ToList(); beatmap.SetBookmarks(newBookmarks); UpdateProgressBar(worker, 45); } // Make new timingpoints List <TimingPointsChange> timingPointsChanges = new List <TimingPointsChange>(); // Add redlines var redlines = timing.Redlines; foreach (TimingPoint tp in redlines) { timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true, meter: true, unInherited: true, omitFirstBarLine: true, fuzzyness: Precision.DOUBLE_EPSILON)); } UpdateProgressBar(worker, 55); // Add SliderVelocity changes for taiko and mania if (mode == GameMode.Taiko || mode == GameMode.Mania) { foreach (TimingPoint tp in svChanges) { timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true)); } } UpdateProgressBar(worker, 60); // Add Kiai toggles foreach (TimingPoint tp in kiaiToggles) { timingPointsChanges.Add(new TimingPointsChange(tp, kiai: true)); } UpdateProgressBar(worker, 65); // Add Hitobject stuff foreach (HitObject ho in beatmap.HitObjects) { if (ho.IsSlider) // SliderVelocity changes { TimingPoint tp = ho.TimingPoint.Copy(); tp.Offset = ho.Time; tp.MpB = ho.SliderVelocity; timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true)); } // Body hitsounds bool vol = (ho.IsSlider && args.VolumeSliders) || (ho.IsSpinner && args.VolumeSpinners); bool sam = (ho.IsSlider && args.SampleSetSliders && ho.SampleSet == 0); bool ind = (ho.IsSlider && args.SampleSetSliders); bool samplesetActuallyChanged = false; foreach (TimingPoint tp in ho.BodyHitsounds) { if (tp.Volume == 5 && args.RemoveMuting) { vol = false; // Removing sliderbody silencing ind = false; // Removing silent custom index } timingPointsChanges.Add(new TimingPointsChange(tp, volume: vol, index: ind, sampleset: sam)); if (tp.SampleSet != ho.HitsoundTimingPoint.SampleSet) { samplesetActuallyChanged = args.SampleSetSliders && ho.SampleSet == 0; } // True for sampleset change in sliderbody } if (ho.IsSlider && (!samplesetActuallyChanged) && ho.SampleSet == 0) // Case can put sampleset on sliderbody { ho.SampleSet = ho.HitsoundTimingPoint.SampleSet; } if (ho.IsSlider && samplesetActuallyChanged) // Make it start out with the right sampleset { TimingPoint tp = ho.HitsoundTimingPoint.Copy(); tp.Offset = ho.Time; timingPointsChanges.Add(new TimingPointsChange(tp, sampleset: true)); } } UpdateProgressBar(worker, 75); // Add timeline hitsounds foreach (TimelineObject tlo in timeline.TimelineObjects) { // Change the samplesets in the hitobjects if (tlo.Origin.IsCircle) { tlo.Origin.SampleSet = tlo.FenoSampleSet; tlo.Origin.AdditionSet = tlo.FenoAdditionSet; if (mode == GameMode.Mania) { tlo.Origin.CustomIndex = tlo.FenoCustomIndex; tlo.Origin.SampleVolume = tlo.FenoSampleVolume; } } else if (tlo.Origin.IsSlider) { tlo.Origin.EdgeHitsounds[tlo.Repeat] = tlo.GetHitsounds(); tlo.Origin.EdgeSampleSets[tlo.Repeat] = tlo.FenoSampleSet; tlo.Origin.EdgeAdditionSets[tlo.Repeat] = tlo.FenoAdditionSet; if (tlo.Origin.EdgeAdditionSets[tlo.Repeat] == tlo.Origin.EdgeSampleSets[tlo.Repeat]) // Simplify additions to auto { tlo.Origin.EdgeAdditionSets[tlo.Repeat] = 0; } } else if (tlo.Origin.IsSpinner) { if (tlo.Repeat == 1) { tlo.Origin.SampleSet = tlo.FenoSampleSet; tlo.Origin.AdditionSet = tlo.FenoAdditionSet; } } else if (tlo.Origin.IsHoldNote) { if (tlo.Repeat == 0) { tlo.Origin.SampleSet = tlo.FenoSampleSet; tlo.Origin.AdditionSet = tlo.FenoAdditionSet; tlo.Origin.CustomIndex = tlo.FenoCustomIndex; tlo.Origin.SampleVolume = tlo.FenoSampleVolume; } } if (tlo.Origin.AdditionSet == tlo.Origin.SampleSet) // Simplify additions to auto { tlo.Origin.AdditionSet = 0; } if (tlo.HasHitsound) // Add greenlines for custom indexes and volumes { TimingPoint tp = tlo.HitsoundTimingPoint.Copy(); bool doUnmute = tlo.FenoSampleVolume == 5 && args.RemoveMuting; bool doMute = args.RemoveUnclickableHitsounds && !args.RemoveMuting && !(tlo.IsCircle || tlo.IsSliderHead || tlo.IsHoldnoteHead); bool ind = !tlo.UsesFilename && !doUnmute; // Index doesnt have to change if custom is overridden by Filename bool vol = !doUnmute; // Remove volume change muted // Index doesn't have to change if the sample it plays currently is the same as the sample it would play with the previous index if (ind) { List <string> nativeSamples = tlo.GetFirstPlayingFilenames(mode, mapDir, firstSamples); int oldIndex = tlo.FenoCustomIndex; int newIndex = tlo.FenoCustomIndex; double latest = double.NegativeInfinity; foreach (TimingPointsChange tpc in timingPointsChanges) { if (tpc.Index && tpc.MyTP.Offset <= tlo.Time && tpc.MyTP.Offset >= latest) { newIndex = tpc.MyTP.SampleIndex; latest = tpc.MyTP.Offset; } } tp.SampleIndex = newIndex; tlo.GiveHitsoundTimingPoint(tp); List <string> newSamples = tlo.GetFirstPlayingFilenames(mode, mapDir, firstSamples); if (nativeSamples.SequenceEqual(newSamples)) { // Index changes dont change sound tp.SampleIndex = newIndex; } else { tp.SampleIndex = oldIndex; } tlo.GiveHitsoundTimingPoint(tp); } tp.Offset = tlo.Time; tp.SampleIndex = tlo.FenoCustomIndex; tp.Volume = doMute ? 5 : tlo.FenoSampleVolume; timingPointsChanges.Add(new TimingPointsChange(tp, volume: vol, index: ind)); } } UpdateProgressBar(worker, 85); // Replace the old timingpoints timing.Clear(); TimingPointsChange.ApplyChanges(timing, timingPointsChanges); beatmap.GiveObjectsGreenlines(); UpdateProgressBar(worker, 90); // Remove unused samples if (args.RemoveUnusedSamples) { RemoveUnusedSamples(mapDir); } // Complete progressbar UpdateProgressBar(worker, 100); return(new MapCleanerResult(objectsResnapped, samplesRemoved)); }