private void ImportFromBeatmap(string importPath)
        {
            try {
                var editor  = new BeatmapEditor(importPath);
                var beatmap = editor.Beatmap;

                Artist          = beatmap.Metadata["ArtistUnicode"].StringValue;
                RomanisedArtist = beatmap.Metadata["Artist"].StringValue;
                Title           = beatmap.Metadata["TitleUnicode"].StringValue;
                RomanisedTitle  = beatmap.Metadata["Title"].StringValue;
                BeatmapCreator  = beatmap.Metadata["Creator"].StringValue;
                Source          = beatmap.Metadata["Source"].StringValue;
                Tags            = beatmap.Metadata["Tags"].StringValue;

                PreviewTime  = beatmap.General["PreviewTime"].Value;
                ComboColours = new ObservableCollection <ComboColour>(beatmap.ComboColours);
                SpecialColours.Clear();
                foreach (var specialColour in beatmap.SpecialColours)
                {
                    SpecialColours.Add(new SpecialColour(specialColour.Value.Color, specialColour.Key));
                }
            }
            catch (Exception ex) {
                MessageBox.Show($"{ex.Message}{Environment.NewLine}{ex.StackTrace}", "Error");
            }
        }
Example #2
0
        public OsuPattern FromFile(string filePath, string name)
        {
            // Read some stuff from the pattern
            var patternBeatmap = new BeatmapEditor(filePath).Beatmap;

            return(FromBeatmap(patternBeatmap, name));
        }
Example #3
0
 public SpectrogramForm(BeatmapEditor editor)
 {
     InitializeComponent();
     Editor = editor;
     Size   = new Size(Screen.PrimaryScreen.Bounds.Width, Size.Height);
     Editor.BeatmapModified += UpdateSpectrogram;
     Editor.BeatmapSwitched += UpdateSpectrogram;
     UpdateSpectrogram(this, EventArgs.Empty);
     Instance = this;
 }
Example #4
0
        public void UnchangingEmptyMapCodeTest()
        {
            var path   = "Resources\\EmptyTestMap.osu";
            var lines  = File.ReadAllLines(path).ToList();
            var editor = new BeatmapEditor(path);
            var lines2 = editor.Beatmap.GetLines();

            for (int i = 0; i < lines.Count; i++)
            {
                Assert.AreEqual(lines[i], lines2[i]);
            }
        }
        /// <summary>
        /// Returns an editor for the beatmap of the specified path. If said beatmap is currently open in the editor it will update the Beatmap object with the latest values.
        /// </summary>
        /// <param name="path">Path to the beatmap</param>
        /// <param name="newestEditor">The editor with the newest version</param>
        /// <param name="selected">List of selected hit objects</param>
        /// <param name="fullReader">Reader object that has already fetched all</param>
        /// <returns>Boolean indicating whether it actually got the newest version</returns>
        public static bool TryGetNewestVersion(string path, out BeatmapEditor newestEditor, out List <HitObject> selected, EditorReader fullReader = null)
        {
            BeatmapEditor editor = new BeatmapEditor(path);

            newestEditor = editor;
            selected     = new List <HitObject>();

            // Check if Editor Reader is enabled
            if (!SettingsManager.Settings.UseEditorReader)
            {
                return(false);
            }

            // Get a reader object that has everything fetched
            var reader = fullReader;

            if (reader == null)
            {
                if (!TryGetFullEditorReader(out reader))
                {
                    return(false);
                }
            }

            // Get the path from the beatmap in memory
            // This can only crash if the provided fullReader didn't fetch all values
            try
            {
                string songs      = SettingsManager.GetSongsPath();
                string folder     = reader.ContainingFolder;
                string filename   = reader.Filename;
                string memoryPath = Path.Combine(songs, folder, filename);

                // Check whether the beatmap in the editor is the same as the beatmap you want
                if (memoryPath != path)
                {
                    return(true);
                }

                // Update the beatmap with memory values
                selected = UpdateBeatmap(editor.Beatmap, reader);
            }
            catch (Exception ex)
            {
                MessageBox.Show($"Exception ({ex.Message}) while editor reading.");
                return(false);
            }

            return(true);
        }
        public static BeatmapEditor TryGetNewestVersion(EditorReader reader, out List <HitObject> selected)
        {
            // Get the path from the beatmap in memory
            string songs      = SettingsManager.GetSongsPath();
            string folder     = reader.ContainingFolder;
            string filename   = reader.Filename;
            string memoryPath = Path.Combine(songs, folder, filename);

            var editor = new BeatmapEditor(memoryPath);

            // Update the beatmap with memory values
            selected = SettingsManager.Settings.UseEditorReader ? UpdateBeatmap(editor.Beatmap, reader) : new List <HitObject>();

            return(editor);
        }
Example #7
0
        /// <summary>
        /// Copies a backup to replace a beatmap at the destination path.
        /// </summary>
        /// <param name="backupPath">Path to the backup map.</param>
        /// <param name="destination">Path to the destination map.</param>
        /// <param name="allowDifferentFilename">If false, this method throws an exception when the backup and the destination have mismatching beatmap metadata.</param>
        public static void LoadMapBackup(string backupPath, string destination, bool allowDifferentFilename = false)
        {
            var backupEditor      = new BeatmapEditor(backupPath);
            var destinationEditor = new BeatmapEditor(destination);

            var backupFilename      = backupEditor.Beatmap.GetFileName();
            var destinationFilename = destinationEditor.Beatmap.GetFileName();

            if (!allowDifferentFilename && !string.Equals(backupFilename, destinationFilename))
            {
                throw new BeatmapIncompatibleException($"The backup and the destination beatmap have mismatching metadata.\n{backupFilename}\n{destinationFilename}");
            }

            File.Copy(backupPath, destination, true);
        }
        public void ImportComboColoursFromBeatmap(string importPath)
        {
            try {
                var editor  = new BeatmapEditor(importPath);
                var beatmap = editor.Beatmap;

                ComboColours.Clear();
                for (int i = 0; i < beatmap.ComboColours.Count; i++)
                {
                    ComboColours.Add(new SpecialColour(beatmap.ComboColours[i].Color, $"Combo{i + 1}"));
                }
            }
            catch (Exception ex) {
                ex.Show();
            }
        }
Example #9
0
        public void ImportComboColoursFromBeatmap(string importPath)
        {
            try {
                var editor  = new BeatmapEditor(importPath);
                var beatmap = editor.Beatmap;

                ComboColours.Clear();
                for (int i = 0; i < beatmap.ComboColours.Count; i++)
                {
                    ComboColours.Add(new SpecialColour(beatmap.ComboColours[i].Color, $"Combo{i + 1}"));
                }
            }
            catch (Exception ex) {
                MessageBox.Show($"{ex.Message}{Environment.NewLine}{ex.StackTrace}", "Error");
            }
        }
Example #10
0
        /// <summary>
        /// Returns an editor for the beatmap which is currently open in the editor. Returns null if there is no beatmap open in the editor.
        /// </summary>
        /// <param name="fullReader">Reader object that has already fetched all</param>
        /// <param name="selected">List of selected hit objects</param>
        /// <returns>An editor for the beatmap</returns>
        /// <exception cref="ArgumentNullException"></exception>
        public static BeatmapEditor GetBeatmapEditor(EditorReader fullReader, out List <HitObject> selected)
        {
            if (fullReader == null)
            {
                throw new ArgumentNullException(nameof(fullReader));
            }

            // Get the path from the beatmap in memory
            string memoryPath = GetCurrentBeatmap(fullReader);

            // Update the beatmap with memory values
            var editor = new BeatmapEditor(memoryPath);

            selected = UpdateBeatmap(editor.Beatmap, fullReader);

            return(editor);
        }
        /// <summary>
        /// Tries to get the newest version if editorRead is true otherwise just makes a normal <see cref="BeatmapEditor"/>
        /// </summary>
        /// <param name="path"></param>
        /// <param name="fullReader"></param>
        /// <param name="readEditor"></param>
        /// <param name="selected"></param>
        /// <param name="editorRead">Indicates true if the editor memory was actually read</param>
        /// <returns></returns>
        public static BeatmapEditor GetBeatmapEditor(string path, EditorReader fullReader, bool readEditor, out List <HitObject> selected, out bool editorRead)
        {
            BeatmapEditor editor;

            if (readEditor)
            {
                editorRead = TryGetNewestVersion(path, out editor, out selected, fullReader);
            }
            else
            {
                editor     = new BeatmapEditor(path);
                selected   = new List <HitObject>();
                editorRead = false;
            }

            return(editor);
        }
Example #12
0
        /// <summary>
        /// Returns an editor for the beatmap of the specified path. If said beatmap is currently open in the editor it will update the Beatmap object with the latest values.
        /// </summary>
        /// <param name="path">Path to the beatmap</param>
        /// <param name="selected">List of selected hit objects</param>
        /// <param name="fullReader">Reader object that has already fetched all</param>
        /// <returns>The editor with the newest version</returns>
        /// <exception cref="ArgumentNullException"></exception>
        public static BeatmapEditor GetNewestVersion(string path, EditorReader fullReader, out List <HitObject> selected)
        {
            if (fullReader == null)
            {
                throw new ArgumentNullException(nameof(fullReader));
            }

            BeatmapEditor editor = new BeatmapEditor(path);

            selected = new List <HitObject>();

            // Get the path from the beatmap in memory
            string memoryPath = GetCurrentBeatmap(fullReader);

            // Check whether the beatmap in the editor is the same as the beatmap you want
            if (memoryPath == path)
            {
                // Update the beatmap with memory values
                selected = UpdateBeatmap(editor.Beatmap, fullReader);
            }

            return(editor);
        }
        public static List <HitObject> GetSelectedObjects(BeatmapEditor editor, EditorReader reader)
        {
            try
            {
                string songs      = SettingsManager.GetSongsPath();
                string folder     = reader.ContainingFolder;
                string filename   = reader.Filename;
                string memoryPath = Path.Combine(songs, folder, filename);

                // Check whether the beatmap in the editor is the same as the beatmap you want
                if (memoryPath != editor.Path)
                {
                    return(new List <HitObject>());
                }

                reader.FetchSelected();
                var convertedSelected  = reader.selectedObjects.Select(o => (HitObject)o).ToList();
                var selectedHitObjects = new List <HitObject>(convertedSelected.Count());
                var comparer           = new HitObjectComparer();

                // Get all the hit objects that are selected according to the editor reader
                foreach (var ho in editor.Beatmap.HitObjects)
                {
                    if (convertedSelected.Contains(ho, comparer))
                    {
                        selectedHitObjects.Add(ho);
                    }
                }
                return(selectedHitObjects);
            }
            catch (Exception ex)
            {
                MessageBox.Show($"Exception ({ex.Message}) while editor reading.");
                return(new List <HitObject>());
            }
        }
        /// <summary>
        /// Returns an editor for the beatmap which is currently open in the editor. Returns null if there is no beatmap open in the editor.
        /// </summary>
        /// <param name="selected">List of selected hit objects</param>
        /// <param name="fullReader">Reader object that has already fetched all</param>
        /// <returns>An editor for the beatmap</returns>
        public static BeatmapEditor GetBeatmapEditor(out List <HitObject> selected, EditorReader fullReader = null)
        {
            selected = new List <HitObject>();
            BeatmapEditor editor = null;

            // Get a reader object that has everything fetched
            var reader = fullReader;

            if (reader == null)
            {
                if (!TryGetFullEditorReader(out reader))
                {
                    return(null);
                }
            }

            // Get the path from the beatmap in memory
            // This can only crash if the provided fullReader didn't fetch all values
            try
            {
                string songs      = SettingsManager.GetSongsPath();
                string folder     = reader.ContainingFolder;
                string filename   = reader.Filename;
                string memoryPath = Path.Combine(songs, folder, filename);

                // Update the beatmap with memory values
                editor   = new BeatmapEditor(memoryPath);
                selected = UpdateBeatmap(editor.Beatmap, reader);
            }
            catch (Exception ex)
            {
                MessageBox.Show($"Exception ({ex.Message}) while editor reading.");
            }

            return(editor);
        }
Example #15
0
        public OsuPattern FromFileFilter(string filePath, out Beatmap patternBeatmap, string name,
                                         string filter = null, double filterStartTime = -1, double filterEndTime = -1)
        {
            // Read some stuff from the pattern
            patternBeatmap = new BeatmapEditor(filePath).Beatmap;

            RemoveStoryboard(patternBeatmap);

            // Optionally filter stuff
            var hitObjects = !string.IsNullOrEmpty(filter) ? patternBeatmap.QueryTimeCode(filter).ToList() : patternBeatmap.HitObjects;

            if (filterStartTime != -1)
            {
                hitObjects.RemoveAll(o => o.EndTime < filterStartTime);
            }
            if (filterEndTime != -1)
            {
                hitObjects.RemoveAll(o => o.Time > filterEndTime);
            }

            RemoveEverythingThatIsNotTheseHitObjects(patternBeatmap, hitObjects);

            return(FromBeatmap(patternBeatmap, name));
        }
Example #16
0
        public void ImportColourHaxFromBeatmap(string importPath)
        {
            try {
                var editor  = new BeatmapEditor(importPath);
                var beatmap = editor.Beatmap;

                // Add default colours if there are no colours
                if (beatmap.ComboColours.Count == 0)
                {
                    beatmap.ComboColours.AddRange(ComboColour.GetDefaultComboColours());
                }

                ComboColours.Clear();
                for (int i = 0; i < beatmap.ComboColours.Count; i++)
                {
                    ComboColours.Add(new SpecialColour(beatmap.ComboColours[i].Color, $"Combo{i + 1}"));
                }

                // Remove all colour points since those are getting replaced
                ColourPoints.Clear();

                // Get all the hit objects which can colorhax. AKA new combos and not spinners
                var colorHaxObjects = beatmap.HitObjects.Where(o => o.ActualNewCombo && !o.IsSpinner).ToArray();

                // Get the array with all the lengths of sequences that are going to be checked
                var sequenceLengthChecks = Enumerable.Range(1, ComboColours.Count * 2 + 2).ToArray();

                int   sequenceStartIndex = 0;
                int[] lastNormalSequence = null;
                bool  lastBurst          = false;
                while (sequenceStartIndex < colorHaxObjects.Length)
                {
                    var firstComboHitObject = colorHaxObjects[sequenceStartIndex];

                    var bestSequence = GetBestSequenceAtIndex(
                        sequenceStartIndex,
                        3,
                        colorHaxObjects,
                        beatmap,
                        sequenceLengthChecks,
                        lastBurst,
                        lastNormalSequence
                        )?.Item1;

                    if (bestSequence == null)
                    {
                        lastBurst           = false;
                        sequenceStartIndex += 1;
                        continue;
                    }

                    var bestContribution = GetSequenceContribution(colorHaxObjects, sequenceStartIndex, bestSequence);

                    // Get the colours for every colour index. Using modulo to make sure the index is always in range.
                    var colourSequence = bestSequence.Select(o => ComboColours[MathHelper.Mod(o, ComboColours.Count)]);

                    // Add a new colour point
                    var mode = bestContribution == 1 &&
                               GetComboLength(beatmap.HitObjects, firstComboHitObject) <= MaxBurstLength
                        ? ColourPointMode.Burst
                        : ColourPointMode.Normal;

                    // To optimize on colour points, we dont add a new colour point if the previous point was a burst and
                    // the sequence before the burst is equivalent to this sequence
                    if (!(lastBurst && lastNormalSequence != null && IsSubSequence(bestSequence, lastNormalSequence) &&
                          (bestSequence.Length == lastNormalSequence.Length || bestContribution <= bestSequence.Length)))
                    {
                        ColourPoints.Add(GenerateNewColourPoint(firstComboHitObject.Time, colourSequence, mode));
                    }

                    lastBurst           = mode == ColourPointMode.Burst;
                    sequenceStartIndex += bestContribution;
                    lastNormalSequence  = mode == ColourPointMode.Burst ? lastNormalSequence : bestSequence;
                }
            }
            catch (Exception ex) {
                MessageBox.Show($"{ex.Message}{Environment.NewLine}{ex.StackTrace}", "Error");
            }
        }
Example #17
0
        /// <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;
            Timeline timeline = beatmap.GetTimeline();

            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 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.Snap1, args.Snap2);
                    if (resnapped)
                    {
                        objectsResnapped += 1;
                    }
                    ho.ResnapEnd(timing, args.Snap1, args.Snap2);
                    ho.ResnapPosition(mode, circleSize);
                }
                UpdateProgressBar(worker, 18);

                // Resnap Kiai toggles
                foreach (TimingPoint tp in kiaiToggles)
                {
                    tp.ResnapSelf(timing, args.Snap1, args.Snap2);
                }
                UpdateProgressBar(worker, 27);

                // Resnap SliderVelocity changes
                foreach (TimingPoint tp in svChanges)
                {
                    tp.ResnapSelf(timing, args.Snap1, args.Snap2);
                }
                UpdateProgressBar(worker, 36);
            }

            if (args.ResnapBookmarks)
            {
                // Resnap the bookmarks
                List <double> bookmarks    = beatmap.GetBookmarks();
                List <double> newBookmarks = bookmarks.Select(o => timing.Resnap(o, args.Snap1, args.Snap2)).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
            List <TimingPoint> redlines = timing.GetAllRedlines();

            foreach (TimingPoint tp in redlines)
            {
                timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true, meter: true, unInherited: true, omitFirstBarLine: true));
            }
            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.TimingPoints.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));
        }
Example #18
0
        public static int RemoveUnusedSamples(string mapDir)
        {
            // Collect all the used samples
            HashSet <string> allFilenames = new HashSet <string>();
            bool             anySpinners  = false;

            List <string> beatmaps = Directory.GetFiles(mapDir, "*.osu", SearchOption.TopDirectoryOnly).ToList();

            foreach (string path in beatmaps)
            {
                BeatmapEditor editor  = new BeatmapEditor(path);
                Beatmap       beatmap = editor.Beatmap;

                GameMode mode           = (GameMode)beatmap.General["Mode"].IntValue;
                double   sliderTickRate = beatmap.Difficulty["SliderTickRate"].DoubleValue;

                if (!anySpinners)
                {
                    anySpinners = mode == 0 && beatmap.HitObjects.Any(o => o.IsSpinner);
                }

                allFilenames.Add(beatmap.General["AudioFilename"].Value.Trim());

                foreach (HitObject ho in beatmap.HitObjects)
                {
                    allFilenames.UnionWith(ho.GetPlayingBodyFilenames(sliderTickRate, false));
                }

                foreach (TimelineObject tlo in beatmap.GetTimeline().TimelineObjects)
                {
                    allFilenames.UnionWith(tlo.GetPlayingFilenames(mode, false));
                }

                foreach (StoryboardSoundSample sbss in beatmap.StoryboardSoundSamples)
                {
                    allFilenames.Add(sbss.FilePath);
                }
            }

            List <string> storyboards = Directory.GetFiles(mapDir, "*.osb", SearchOption.TopDirectoryOnly).ToList();

            foreach (string path in storyboards)
            {
                StoryboardEditor editor     = new StoryboardEditor(path);
                StoryBoard       storyboard = editor.StoryBoard;

                foreach (StoryboardSoundSample sbss in storyboard.StoryboardSoundSamples)
                {
                    allFilenames.Add(sbss.FilePath);
                }
            }

            // Only if there are spinners in standard you may have spinnerspin and spinnerbonus
            if (anySpinners)
            {
                allFilenames.UnionWith(new[] { "spinnerspin", "spinnerbonus" });
            }

            // We don't do extensions in osu!
            HashSet <string> usedFilenames = new HashSet <string>(allFilenames.Select(Path.GetFileNameWithoutExtension));

            // Get the sound files
            var             extList     = new[] { ".wav", ".ogg", ".mp3" };
            DirectoryInfo   di          = new DirectoryInfo(mapDir);
            List <FileInfo> sampleFiles = di.GetFiles("*.*", SearchOption.TopDirectoryOnly)
                                          .Where(n => extList.Contains(n.Extension, StringComparer.OrdinalIgnoreCase)).ToList();

            int removed = 0;

            foreach (FileInfo fi in sampleFiles)
            {
                string extless = Path.GetFileNameWithoutExtension(fi.Name);
                if (!(usedFilenames.Contains(extless) || BeatmapSkinnableSamples.Any(o => Regex.IsMatch(extless, o))))
                {
                    fi.Delete();
                    //Console.WriteLine($"Deleting sample {fi.Name}");
                    removed++;
                }
            }

            return(removed);
        }
Example #19
0
        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!");
        }
 /// <summary>
 /// Returns an editor for the beatmap of the specified path. If said beatmap is currently open in the editor it will update the Beatmap object with the latest values.
 /// </summary>
 /// <param name="path">Path to the beatmap</param>
 /// <param name="editor">The editor with the newest version</param>
 /// <param name="fullReader">Reader object that has already fetched all</param>
 /// <returns>Boolean indicating whether it actually got the newest version</returns>
 public static bool TryGetNewestVersion(string path, out BeatmapEditor editor, EditorReader fullReader = null)
 {
     return(TryGetNewestVersion(path, out editor, out _, fullReader));
 }
Example #21
0
        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!");
        }