diff --git a/Mapping Tools/Classes/BeatmapHelper/Beatmap.cs b/Mapping Tools/Classes/BeatmapHelper/Beatmap.cs index 31147f23..5dbbe80a 100644 --- a/Mapping Tools/Classes/BeatmapHelper/Beatmap.cs +++ b/Mapping Tools/Classes/BeatmapHelper/Beatmap.cs @@ -153,6 +153,11 @@ public class Beatmap : ITextFile { /// public List Bookmarks { get => GetBookmarks(); set => SetBookmarks(value); } + /// + /// When true, all coordinates and times will be serialized without rounding. + /// + public bool SaveWithFloatPrecision { get; set; } + /// /// Initializes the Beatmap file format. /// @@ -245,7 +250,7 @@ public void SetLines(List lines) { /// Sorts all hitobjects in map by order of time. /// public void SortHitObjects() { - HitObjects = HitObjects.OrderBy(o => o.Time).ToList(); + HitObjects.Sort(); } /// @@ -253,7 +258,12 @@ public void SortHitObjects() { /// public void CalculateSliderEndTimes() { foreach (var ho in HitObjects.Where(ho => ho.IsSlider)) { - ho.TemporalLength = BeatmapTiming.CalculateSliderTemporalLength(ho.Time, ho.PixelLength); + if (double.IsNaN(ho.PixelLength) || ho.PixelLength < 0 || ho.CurvePoints.All(o => o == ho.Pos)) { + ho.TemporalLength = 0; + } + else { + ho.TemporalLength = BeatmapTiming.CalculateSliderTemporalLength(ho.Time, ho.PixelLength); + } } } @@ -426,7 +436,10 @@ public List GetLines() { lines.AddRange(StoryboardSoundSamples.Select(sbss => sbss.GetLine())); lines.Add(""); lines.Add("[TimingPoints]"); - lines.AddRange(from tp in BeatmapTiming.TimingPoints where tp != null select tp.GetLine()); + lines.AddRange(BeatmapTiming.TimingPoints.Where(tp => tp != null).Select(tp => { + tp.SaveWithFloatPrecision = SaveWithFloatPrecision; + return tp.GetLine(); + })); lines.Add(""); if (ComboColours.Any()) { lines.Add(""); @@ -440,20 +453,68 @@ public List GetLines() { } lines.Add(""); lines.Add("[HitObjects]"); - lines.AddRange(HitObjects.Select(ho => ho.GetLine())); + lines.AddRange(HitObjects.Select(ho => { + ho.SaveWithFloatPrecision = SaveWithFloatPrecision; + return ho.GetLine(); + })); return lines; } + public double GetHitObjectStartTime() { + return HitObjects.Min(h => h.Time); + } + + public double GetHitObjectEndTime() { + return HitObjects.Max(h => h.EndTime); + } + + public void OffsetTime(double offset) { + BeatmapTiming.TimingPoints?.ForEach(tp => tp.Offset += offset); + HitObjects?.ForEach(h => h.MoveTime(offset)); + } + + private IEnumerable EnumerateAllEvents() { + return BackgroundAndVideoEvents.Concat(BreakPeriods).Concat(StoryboardSoundSamples) + .Concat(StoryboardLayerFail).Concat(StoryboardLayerPass).Concat(StoryboardLayerBackground) + .Concat(StoryboardLayerForeground).Concat(StoryboardLayerOverlay); + } + + public double GetLeadInTime() { + var leadInTime = General["AudioLeadIn"].DoubleValue; + var od = Difficulty["OverallDifficulty"].DoubleValue; + var window50 = Math.Ceiling(200 - 10 * od); + var eventsWithStartTime = EnumerateAllEvents().OfType().ToArray(); + if (eventsWithStartTime.Length > 0) + leadInTime = Math.Max(-eventsWithStartTime.Min(o => o.StartTime), leadInTime); + if (HitObjects.Count > 0) { + var approachTime = ApproachRateToMs(Difficulty["ApproachRate"].DoubleValue); + leadInTime = Math.Max(approachTime - HitObjects[0].Time, leadInTime); + } + return leadInTime + window50 + 1000; + } + + public double GetMapStartTime() { + return -GetLeadInTime(); + } + + public double GetMapEndTime() { + var endTime = HitObjects.Count > 0 + ? Math.Max(GetHitObjectEndTime() + 200, HitObjects.Last().EndTime + 3000) + : double.NegativeInfinity; + var eventsWithEndTime = EnumerateAllEvents().OfType().ToArray(); + if (eventsWithEndTime.Length > 0) + endTime = Math.Max(endTime, eventsWithEndTime.Max(o => o.EndTime) - 500); + return endTime; + } + /// - /// Grabs the specified file name of beatmap file. - /// with format of: - /// Artist - Title (Host) [Difficulty].osu + /// Gets the time at which auto-fail gets checked by osu! + /// The counted judgements must add up to the object count at this time. /// - /// String of file name. - public string GetFileName() { - return GetFileName(Metadata["Artist"].Value, Metadata["Title"].Value, - Metadata["Creator"].Value, Metadata["Version"].Value); + /// + public double GetAutoFailCheckTime() { + return GetHitObjectEndTime() + 200; } /// @@ -502,6 +563,17 @@ public IEnumerable QueryTimeCode(string code) { } } + /// + /// Grabs the specified file name of beatmap file. + /// with format of: + /// Artist - Title (Host) [Difficulty].osu + /// + /// String of file name. + public string GetFileName() { + return GetFileName(Metadata["Artist"].Value, Metadata["Title"].Value, + Metadata["Creator"].Value, Metadata["Version"].Value); + } + /// /// Grabs the specified file name of beatmap file. /// with format of: @@ -554,5 +626,13 @@ private static List GetCategoryLines(List lines, string category } return categoryLines; } + + public Beatmap DeepCopy() { + var newBeatmap = (Beatmap)MemberwiseClone(); + newBeatmap.HitObjects = HitObjects?.Select(h => h.DeepCopy()).ToList(); + newBeatmap.BeatmapTiming = new Timing(BeatmapTiming.TimingPoints.Select(t => t.Copy()).ToList(), BeatmapTiming.SliderMultiplier); + newBeatmap.GiveObjectsGreenlines(); + return newBeatmap; + } } } diff --git a/Mapping Tools/Classes/BeatmapHelper/ComboColour.cs b/Mapping Tools/Classes/BeatmapHelper/ComboColour.cs index 76280978..8bdf5ac0 100644 --- a/Mapping Tools/Classes/BeatmapHelper/ComboColour.cs +++ b/Mapping Tools/Classes/BeatmapHelper/ComboColour.cs @@ -50,6 +50,9 @@ public ComboColour(string line) { Color = Color.FromRgb((byte)r, (byte)g, (byte)b); } + public ComboColour Copy() { + return (ComboColour) MemberwiseClone(); + } /// Returns a string that represents the current object. /// A string that represents the current object. diff --git a/Mapping Tools/Classes/BeatmapHelper/Events/StoryboardSoundSample.cs b/Mapping Tools/Classes/BeatmapHelper/Events/StoryboardSoundSample.cs index a8478dde..50cbc415 100644 --- a/Mapping Tools/Classes/BeatmapHelper/Events/StoryboardSoundSample.cs +++ b/Mapping Tools/Classes/BeatmapHelper/Events/StoryboardSoundSample.cs @@ -8,7 +8,7 @@ namespace Mapping_Tools.Classes.BeatmapHelper.Events { /// /// Sample,56056,0,"soft-hitnormal.wav",30 /// - public class StoryboardSoundSample : Event, IEquatable, IHasStartTime { + public class StoryboardSoundSample : Event, IEquatable, IHasStartTime, IHasEndTime { /// /// The time when this sound event occurs. /// @@ -93,5 +93,10 @@ public bool Equals(StoryboardSoundSample other) { FilePath == other.FilePath && Volume == other.Volume); } + + public int EndTime { + get => StartTime; + set => StartTime = value; + } } } diff --git a/Mapping Tools/Classes/BeatmapHelper/HitObject.cs b/Mapping Tools/Classes/BeatmapHelper/HitObject.cs index 12d29de0..4bec8228 100644 --- a/Mapping Tools/Classes/BeatmapHelper/HitObject.cs +++ b/Mapping Tools/Classes/BeatmapHelper/HitObject.cs @@ -13,12 +13,7 @@ namespace Mapping_Tools.Classes.BeatmapHelper { /// /// [JsonObject(MemberSerialization.OptIn)] - public class HitObject : ITextLine { - public List BodyHitsounds = new List(); - private int _repeat; - - // Special combined with timeline - public List TimelineObjects = new List(); + public class HitObject : ITextLine, IComparable { public HitObject() { } @@ -202,6 +197,14 @@ public double EndTime { set => SetEndTime(value); } // Includes all repeats + private double GetEndTime() { + return Math.Floor(Time + TemporalLength * Repeat + Precision.DOUBLE_EPSILON); + } + + private void SetEndTime(double value) { + TemporalLength = Repeat == 0 ? 0 : (value - Time) / Repeat; + } + // Special combined with greenline [JsonProperty] public double SliderVelocity { get; set; } @@ -215,6 +218,17 @@ public double EndTime { [JsonProperty] public bool IsSelected { get; set; } + public List BodyHitsounds = new List(); + private int _repeat; + + // Special combined with timeline + public List TimelineObjects = new List(); + + /// + /// When true, all coordinates and times will be serialized without rounding. + /// + public bool SaveWithFloatPrecision { get; set; } + /// public void SetLine(string line) { @@ -335,9 +349,9 @@ public void SetLine(string line) { /// public string GetLine() { var values = new List { - Pos.X.ToRoundInvariant(), - Pos.Y.ToRoundInvariant(), - Time.ToRoundInvariant(), + SaveWithFloatPrecision ? Pos.X.ToInvariant() : Pos.X.ToRoundInvariant(), + SaveWithFloatPrecision ? Pos.Y.ToInvariant() : Pos.Y.ToRoundInvariant(), + SaveWithFloatPrecision ? Time.ToInvariant() : Time.ToRoundInvariant(), ObjectType.ToInvariant(), Hitsounds.ToInvariant() }; @@ -345,7 +359,7 @@ public string GetLine() { if (IsSlider) { var builder = new StringBuilder(); builder.Append(GetPathTypeString()); - foreach (var p in CurvePoints) builder.Append($"|{p.X.ToRoundInvariant()}:{p.Y.ToRoundInvariant()}"); + foreach (var p in CurvePoints) builder.Append($"|{(SaveWithFloatPrecision ? p.X.ToInvariant() : p.X.ToRoundInvariant())}:{(SaveWithFloatPrecision ? p.Y.ToInvariant() : p.Y.ToRoundInvariant())}"); values.Add(builder.ToString()); values.Add(Repeat.ToInvariant()); values.Add(PixelLength.ToInvariant()); @@ -364,7 +378,7 @@ public string GetLine() { values.Add(Extras); } } else if (IsSpinner) { - values.Add(EndTime.ToRoundInvariant()); + values.Add(SaveWithFloatPrecision ? EndTime.ToInvariant() : EndTime.ToRoundInvariant()); values.Add(Extras); } else { // It's a circle or a hold note @@ -375,14 +389,6 @@ public string GetLine() { return string.Join(",", values); } - private double GetEndTime() { - return Math.Floor(Time + TemporalLength * Repeat + Precision.DOUBLE_EPSILON); - } - - private void SetEndTime(double value) { - TemporalLength = Repeat == 0 ? 0 : (value - Time) / Repeat; - } - /// /// /// @@ -475,7 +481,6 @@ public List GetAllTloTimes(Timing timing) { /// public void MoveTime(double deltaTime) { Time += deltaTime; - EndTime += deltaTime; // Move its timelineobjects foreach (var tlo in TimelineObjects) tlo.Time += deltaTime; @@ -643,7 +648,7 @@ public void SetHitsounds(int hitsounds) { public string GetExtras() { if (IsHoldNote) - return string.Join(":", EndTime.ToRoundInvariant(), SampleSet.ToIntInvariant(), + return string.Join(":", SaveWithFloatPrecision ? EndTime.ToInvariant() : EndTime.ToRoundInvariant(), SampleSet.ToIntInvariant(), AdditionSet.ToIntInvariant(), CustomIndex.ToInvariant(), SampleVolume.ToRoundInvariant(), Filename); return string.Join(":", SampleSet.ToIntInvariant(), AdditionSet.ToIntInvariant(), CustomIndex.ToInvariant(), SampleVolume.ToRoundInvariant(), Filename); @@ -758,6 +763,34 @@ private string GetPathTypeString() { } } + /// + /// Detects a failure in the slider path algorithm causing a slider to become invisible. + /// + /// + public bool IsInvisible() { + return PixelLength != 0 && PixelLength <= 0.0001 || + double.IsNaN(PixelLength) || + CurvePoints.All(o => o == Pos); + } + + public HitObject DeepCopy() { + var newHitObject = (HitObject) MemberwiseClone(); + newHitObject.BodyHitsounds = BodyHitsounds?.Select(o => o.Copy()).ToList(); + newHitObject.TimelineObjects = TimelineObjects?.Select(o => o.Copy()).ToList(); + newHitObject.CurvePoints = CurvePoints?.Copy(); + if (EdgeHitsounds != null) + newHitObject.EdgeHitsounds = new List(EdgeHitsounds); + if (EdgeSampleSets != null) + newHitObject.EdgeSampleSets = new List(EdgeSampleSets); + if (EdgeAdditionSets != null) + newHitObject.EdgeAdditionSets = new List(EdgeAdditionSets); + newHitObject.TimingPoint = TimingPoint?.Copy(); + newHitObject.HitsoundTimingPoint = HitsoundTimingPoint?.Copy(); + newHitObject.UnInheritedTimingPoint = UnInheritedTimingPoint?.Copy(); + newHitObject.Colour = Colour?.Copy(); + return newHitObject; + } + public void Debug() { Console.WriteLine(GetLine()); foreach (var tp in BodyHitsounds) { @@ -778,5 +811,12 @@ public void Debug() { Console.WriteLine(@"feno volume: " + tlo.FenoSampleVolume); } } + + public int CompareTo(HitObject other) { + if (ReferenceEquals(this, other)) return 0; + if (ReferenceEquals(null, other)) return 1; + if (Time == other.Time) return other.NewCombo.CompareTo(NewCombo); + return Time.CompareTo(other.Time); + } } } \ No newline at end of file diff --git a/Mapping Tools/Classes/BeatmapHelper/TimelineObject.cs b/Mapping Tools/Classes/BeatmapHelper/TimelineObject.cs index 2525ca90..42530636 100644 --- a/Mapping Tools/Classes/BeatmapHelper/TimelineObject.cs +++ b/Mapping Tools/Classes/BeatmapHelper/TimelineObject.cs @@ -399,6 +399,10 @@ public static string GetFileName(SampleSet sampleSet, Hitsound hitsound, int ind } } + public TimelineObject Copy() { + return (TimelineObject) MemberwiseClone(); + } + public override string ToString() { return $"{Time}, {ObjectType}, {Repeat}, {FenoSampleVolume}"; } diff --git a/Mapping Tools/Classes/BeatmapHelper/Timing.cs b/Mapping Tools/Classes/BeatmapHelper/Timing.cs index 84284f78..ee3954ea 100644 --- a/Mapping Tools/Classes/BeatmapHelper/Timing.cs +++ b/Mapping Tools/Classes/BeatmapHelper/Timing.cs @@ -23,6 +23,13 @@ public class Timing { /// public double SliderMultiplier { get; set; } + /// + public Timing(List timingPoints, double sliderMultiplier) { + TimingPoints = timingPoints; + SliderMultiplier = sliderMultiplier; + Sort(); + } + /// public Timing(List timingLines, double sliderMultiplier) { TimingPoints = GetTimingPoints(timingLines); @@ -37,6 +44,40 @@ public void Sort() { TimingPoints = TimingPoints.OrderBy(o => o.Offset).ThenByDescending(o => o.Uninherited).ToList(); } + /// + /// Calculates the number of beats between the start time and the end time. + /// The resulting number of beats will be rounded to a 1/16 or 1/12 beat divisor. + /// + /// + /// + /// To round the number of beats to a snap divisor. + /// + public double GetBeatLength(double startTime, double endTime, bool round = false) { + var redlines = GetTimingPointsInTimeRange(startTime, endTime) + .Where(tp => tp.Uninherited); + + double beats = 0; + double lastTime = startTime; + var lastRedline = GetRedlineAtTime(startTime); + foreach (var redline in redlines) { + var inc1 = (redline.Offset - lastTime) / lastRedline.MpB; + beats += round ? MultiSnapRound(inc1, 16, 12) : inc1; + + lastTime = redline.Offset; + lastRedline = redline; + } + var inc2 = (endTime - lastTime) / lastRedline.MpB; + beats += round ? MultiSnapRound(inc2, 16, 12) : inc2; + + return beats; + } + + private static double MultiSnapRound(double value, double divisor1, double divisor2) { + var round1 = Math.Round(value * divisor1) / divisor1; + var round2 = Math.Round(value * divisor2) / divisor2; + return Math.Abs(round1 - value) < Math.Abs(round2 - value) ? round1 : round2; + } + /// /// This method calculates time of the tick on the timeline which is nearest to specified time. /// This method is mostly used to snap objects to timing. diff --git a/Mapping Tools/Classes/BeatmapHelper/TimingPoint.cs b/Mapping Tools/Classes/BeatmapHelper/TimingPoint.cs index a0114834..8dd48fb3 100644 --- a/Mapping Tools/Classes/BeatmapHelper/TimingPoint.cs +++ b/Mapping Tools/Classes/BeatmapHelper/TimingPoint.cs @@ -3,6 +3,7 @@ using Mapping_Tools.Classes.MathUtil; using System; using System.Collections; +using Newtonsoft.Json; using static Mapping_Tools.Classes.BeatmapHelper.FileFormatHelper; namespace Mapping_Tools.Classes.BeatmapHelper { @@ -60,6 +61,12 @@ public class TimingPoint : ITextLine { /// public bool OmitFirstBarLine { get; set; } + /// + /// When true, all coordinates and times will be serialized without rounding. + /// + [JsonIgnore] + public bool SaveWithFloatPrecision { get; set; } + /// /// Creates a new /// @@ -163,7 +170,7 @@ public TimingPoint() /// public string GetLine() { int style = MathHelper.GetIntFromBitArray(new BitArray(new[] { Kiai, false, false, OmitFirstBarLine })); - return $"{Offset.ToRoundInvariant()},{MpB.ToInvariant()},{Meter.TempoNumerator.ToInvariant()},{SampleSet.ToIntInvariant()},{SampleIndex.ToInvariant()},{Volume.ToRoundInvariant()},{Convert.ToInt32(Uninherited).ToInvariant()},{style.ToInvariant()}"; + return $"{(SaveWithFloatPrecision ? Offset.ToInvariant() : Offset.ToRoundInvariant())},{MpB.ToInvariant()},{Meter.TempoNumerator.ToInvariant()},{SampleSet.ToIntInvariant()},{SampleIndex.ToInvariant()},{(SaveWithFloatPrecision ? Volume.ToInvariant() : Volume.ToRoundInvariant())},{Convert.ToInt32(Uninherited).ToInvariant()},{style.ToInvariant()}"; } /// diff --git a/Mapping Tools/Classes/HitsoundStuff/HitsoundExporter.cs b/Mapping Tools/Classes/HitsoundStuff/HitsoundExporter.cs index dcfedeae..599fb0d9 100644 --- a/Mapping Tools/Classes/HitsoundStuff/HitsoundExporter.cs +++ b/Mapping Tools/Classes/HitsoundStuff/HitsoundExporter.cs @@ -42,7 +42,7 @@ public static void ExportHitsounds(List hitsounds, string baseBea // Add red lines List timingPoints = beatmap.BeatmapTiming.GetAllRedlines(); List timingPointsChanges = timingPoints.Select(tp => - new TimingPointsChange(tp, mpb: true, meter: true, inherited: true, omitFirstBarLine: true)) + new TimingPointsChange(tp, mpb: true, meter: true, unInherited: true, omitFirstBarLine: true)) .ToList(); // Add hitsound stuff diff --git a/Mapping Tools/Classes/HitsoundStuff/VorbisFileWriter.cs b/Mapping Tools/Classes/HitsoundStuff/VorbisFileWriter.cs index aad0e9f1..d3e0f159 100644 --- a/Mapping Tools/Classes/HitsoundStuff/VorbisFileWriter.cs +++ b/Mapping Tools/Classes/HitsoundStuff/VorbisFileWriter.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; +using Mapping_Tools.Classes.MathUtil; namespace Mapping_Tools.Classes.HitsoundStuff { public class VorbisFileWriter : IDisposable { @@ -35,7 +36,7 @@ public VorbisFileWriter(Stream outStream, int sampleRate, int channels, float qu var info = VorbisInfo.InitVariableBitRate(channels, sampleRate, quality); // set up our packet->stream encoder - var serial = MainWindow.MainRandom.Next(); + var serial = RNG.Next(); oggStream = new OggStream(serial); // ========================================================= diff --git a/Mapping Tools/Classes/MathUtil/RNG.cs b/Mapping Tools/Classes/MathUtil/RNG.cs index 694a22ce..2e35913f 100644 --- a/Mapping Tools/Classes/MathUtil/RNG.cs +++ b/Mapping Tools/Classes/MathUtil/RNG.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; namespace Mapping_Tools.Classes.MathUtil { /// @@ -117,5 +118,16 @@ public static byte[] NextBytes(int length) { NextBytes(bytes); return bytes; } + + /// + /// Creates a string with random letters and numbers. + /// + /// The length the string should have. + /// The newly created string. + public static string RandomString(int length) { + const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + return new string(Enumerable.Range(1, length) + .Select(_ => chars[random.Next(chars.Length)]).ToArray()); + } } } \ No newline at end of file diff --git a/Mapping Tools/Classes/SnappingTools/Serialization/SnappingToolsSaveSlot.cs b/Mapping Tools/Classes/SnappingTools/Serialization/SnappingToolsSaveSlot.cs index cd249e2e..7c6cb6c7 100644 --- a/Mapping Tools/Classes/SnappingTools/Serialization/SnappingToolsSaveSlot.cs +++ b/Mapping Tools/Classes/SnappingTools/Serialization/SnappingToolsSaveSlot.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Windows.Input; using Mapping_Tools.Annotations; +using Mapping_Tools.Classes.MathUtil; using Mapping_Tools.Classes.SystemTools; using Mapping_Tools.Components.Domain; using Newtonsoft.Json; @@ -81,9 +82,9 @@ public void Dispose() { } private static string GenerateActiveHotkeyHandle() { - var number = MainWindow.MainRandom.Next(int.MaxValue); + var number = RNG.Next(); while (MainWindow.AppWindow.ListenerManager.ActiveHotkeys.ContainsKey($"SaveSlot - {number}")) { - number = MainWindow.MainRandom.Next(int.MaxValue); + number = RNG.Next(); } return $"SaveSlot - {number}"; } diff --git a/Mapping Tools/Classes/Tools/AutoFailDetector.cs b/Mapping Tools/Classes/Tools/AutoFailDetector.cs new file mode 100644 index 00000000..043964d0 --- /dev/null +++ b/Mapping Tools/Classes/Tools/AutoFailDetector.cs @@ -0,0 +1,428 @@ +using Mapping_Tools.Classes.BeatmapHelper; +using Mapping_Tools.Classes.MathUtil; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; + +namespace Mapping_Tools.Classes.Tools { + public class AutoFailDetector { + private class ProblemArea { + public int index; + public HitObject unloadableHitObject; + public HashSet disruptors; + public HashSet timesToCheck; + + public int GetStartTime() { + return (int)unloadableHitObject.Time; + } + + public int GetEndTime() { + return (int)unloadableHitObject.EndTime; + } + } + + private const int maxPaddingCount = 2000; + + private readonly int mapStartTime; + private readonly int mapEndTime; + private readonly int autoFailCheckTime; + private readonly int approachTime; + private readonly int window50; + private readonly int physicsTime; + private List hitObjects; + private List problemAreas; + + private SortedSet timesToCheckStartIndex; + + public List UnloadingObjects; + public List PotentialUnloadingObjects; + public List Disruptors; + + public AutoFailDetector(List hitObjects, int mapStartTime, int mapEndTime, int autoFailCheckTime, int approachTime, int window50, int physicsTime) { + // Sort the hitobjects + SetHitObjects(hitObjects); + + this.mapStartTime = mapStartTime; + this.mapEndTime = mapEndTime; + this.autoFailCheckTime = autoFailCheckTime; + this.approachTime = approachTime; + this.window50 = window50; + this.physicsTime = physicsTime; + } + + private void SortHitObjects() { + hitObjects.Sort(); + } + + public void SetHitObjects(List hitObjects2) { + hitObjects = hitObjects2; + SortHitObjects(); + } + + public bool DetectAutoFail() { + // Initialize lists + UnloadingObjects = new List(); + PotentialUnloadingObjects = new List(); + Disruptors = new List(); + + // Get times to check + // These are all the times at which the startIndex can change in the object loading system. + timesToCheckStartIndex = new SortedSet(hitObjects.SelectMany(ho => new[] { + (int)ho.EndTime + approachTime, + (int)ho.EndTime + approachTime + 1 + })); + + // Find all problematic areas which could cause auto-fail depending on the binary search + // A problem area consists of one object and the objects which can unload it + // An object B can unload another object A if it has a later index than A and an end time earlier than A's end time - approach time. + // A loaded object has to be loaded after its end time for any period long enough for the physics update tick to count the judgement. + // I ignore all unloadable objects B for which at least one unloadable object A is loaded implies B is loaded. In that case I say A contains B. + problemAreas = new List(); + for (int i = 0; i < hitObjects.Count; i++) { + var ho = hitObjects[i]; + var adjEndTime = GetAdjustedEndTime(ho); + var negative = adjEndTime < ho.Time - approachTime; + + // Ignore all problem areas which are contained by another unloadable object, + // because fixing the outer problem area will also fix all of the problems inside. + // Added a check for the end time to prevent weird situations with the endIndex caused by negative duration. + if (problemAreas.Count > 0 && !negative) { + // Lower end time means that it will be loaded alongside the previous problem area. + var lastAdjEndTime = GetAdjustedEndTime(problemAreas.Last().unloadableHitObject); + if (adjEndTime <= lastAdjEndTime) { + continue; + } + + // If the end time is greater but there has been no time to change the start index yet, + // then it is still contained in the previous problem area. + if (timesToCheckStartIndex.GetViewBetween(lastAdjEndTime, adjEndTime + physicsTime).Count == 0) { + continue; + } + } + + // Check all later objects for any which have an early enough end time + var disruptors = new HashSet(); + for (int j = i + 1; j < hitObjects.Count; j++) { + var ho2 = hitObjects[j]; + if (ho2.EndTime < adjEndTime + physicsTime - approachTime) { + disruptors.Add(ho2); + + Disruptors.Add(ho2.Time); + } + } + + if (disruptors.Count == 0 && !negative) + continue; + + // The first time after the end time where the object could be loaded + var firstRequiredLoadTime = adjEndTime; + if (i > 0) + firstRequiredLoadTime = Math.Max(adjEndTime, (int)hitObjects[i - 1].Time - approachTime + 1); + // It cant load before the map has started + firstRequiredLoadTime = Math.Max(firstRequiredLoadTime, mapStartTime); + + // These are all the times to check. If the object is loaded at all these times, then it will not cause auto-fail. (terms and conditions apply) + var timesToCheck = new HashSet(timesToCheckStartIndex.GetViewBetween( + firstRequiredLoadTime, firstRequiredLoadTime + physicsTime)) {firstRequiredLoadTime + physicsTime}; + + problemAreas.Add(new ProblemArea { index = i, unloadableHitObject = ho, disruptors = disruptors, timesToCheck = timesToCheck }); + PotentialUnloadingObjects.Add(ho.Time); + } + + int autoFails = 0; + // Use osu!'s object loading algorithm to find out which objects are actually loaded + foreach (var problemArea in problemAreas) { + foreach (var time in problemArea.timesToCheck) { + var minimalLeft = time - approachTime; + var minimalRight = time + approachTime; + + var startIndex = OsuBinarySearch(minimalLeft); + var endIndex = hitObjects.FindIndex(startIndex, ho => ho.Time > minimalRight); + if (endIndex < 0) { + endIndex = hitObjects.Count - 1; + } + + var hitObjectsMinimal = hitObjects.GetRange(startIndex, 1 + endIndex - startIndex); + + if (!hitObjectsMinimal.Contains(problemArea.unloadableHitObject) || time > autoFailCheckTime) { + UnloadingObjects.Add(problemArea.unloadableHitObject.Time); + autoFails++; + break; + } + } + } + + return autoFails > 0; + } + + private int GetAdjustedEndTime(HitObject ho) { + if (ho.IsCircle) { + return (int)ho.Time + window50; + } + if (ho.IsSlider || ho.IsSpinner) { + return (int)ho.EndTime; + } + + return (int)Math.Max(ho.Time + window50, ho.EndTime); + } + + public bool AutoFailFixDialogue(bool autoPlaceFix) { + if (problemAreas.Count == 0) + return false; + + int[] solution = SolveAutoFailPadding(); + int paddingCount = solution.Sum(); + bool acceptedSolution = false; + int solutionCount = 0; + + foreach (var sol in SolveAutoFailPaddingEnumerableInfinite(paddingCount)) { + solution = sol; + + StringBuilder guideBuilder = new StringBuilder(); + AddFixGuide(guideBuilder, sol); + guideBuilder.AppendLine("\nDo you want to use this solution?"); + + var result = MessageBox.Show(guideBuilder.ToString(), $"Solution {++solutionCount}", MessageBoxButton.YesNoCancel); + if (result == MessageBoxResult.Yes) { + acceptedSolution = true; + break; + } + if (result == MessageBoxResult.Cancel) { + break; + } + } + + + if (autoPlaceFix && acceptedSolution) { + PlaceFixGuide(solution); + return true; + } + + return false; + } + + private void AddFixGuide(StringBuilder guideBuilder, IReadOnlyList paddingSolution) { + guideBuilder.AppendLine("Auto-fail fix guide. Place these extra objects to fix auto-fail:\n"); + int lastTime = 0; + for (int i = 0; i < problemAreas.Count; i++) { + guideBuilder.AppendLine(i == 0 + ? $"Extra objects before {problemAreas[i].GetStartTime()}: {paddingSolution[i]}" + : $"Extra objects between {lastTime} - {problemAreas[i].GetStartTime()}: {paddingSolution[i]}"); + lastTime = GetAdjustedEndTime(problemAreas[i].unloadableHitObject) - approachTime; + } + guideBuilder.AppendLine($"Extra objects after {lastTime}: {paddingSolution.Last()}"); + } + + private void PlaceFixGuide(IReadOnlyList paddingSolution) { + int lastTime = 0; + for (int i = 0; i < problemAreas.Count; i++) { + if (paddingSolution[i] > 0) { + var t = GetSafePlacementTime(lastTime, problemAreas[i].GetStartTime()); + for (int j = 0; j < paddingSolution[i]; j++) { + hitObjects.Add(new HitObject { Pos = Vector2.Zero, Time = t, ObjectType = 8, EndTime = t - 1 }); + } + } + + lastTime = GetAdjustedEndTime(problemAreas[i].unloadableHitObject) - approachTime; + } + + if (paddingSolution.Last() > 0) { + var t = GetSafePlacementTime(lastTime, mapEndTime); + for (int i = 0; i < paddingSolution.Last(); i++) { + hitObjects.Add(new HitObject { Pos = Vector2.Zero, Time = t, ObjectType = 8, EndTime = t - 1 }); + } + } + + SortHitObjects(); + } + + private int GetSafePlacementTime(int start, int end) { + var rangeObjects = hitObjects.FindAll(o => o.EndTime >= start && o.Time <= end); + + for (int i = end - 1; i >= start; i--) { + if (!rangeObjects.Any(ho => + i >= (int)ho.Time && + i <= GetAdjustedEndTime(ho) - approachTime)) { + return i; + } + } + + throw new Exception($"Can't find a safe place to place objects between {start} and {end}."); + } + + private int[] SolveAutoFailPadding(int startPaddingCount = 0) { + int padding = startPaddingCount; + int[] solution; + while (!SolveAutoFailPadding(padding++, out solution)) { + if (padding > maxPaddingCount) { + throw new Exception("No auto-fail fix padding solution found."); + } + } + + return solution; + } + + private bool SolveAutoFailPadding(int paddingCount, out int[] solution) { + solution = new int[problemAreas.Count + 1]; + + int leftPadding = 0; + for (var i = 0; i < problemAreas.Count; i++) { + var problemAreaSolution = + SolveSingleProblemAreaPadding(problemAreas[i], paddingCount, leftPadding); + + if (problemAreaSolution.Count == 0 || problemAreaSolution.Max() < leftPadding) { + return false; + } + + var lowest = problemAreaSolution.First(o => o >= leftPadding); + solution[i] = lowest - leftPadding; + leftPadding = lowest; + } + + solution[problemAreas.Count] = paddingCount - leftPadding; + + return true; + } + + private IEnumerable SolveAutoFailPaddingEnumerableInfinite(int initialPaddingCount) { + int paddingCount = initialPaddingCount; + while (true) { + foreach (var solution in SolveAutoFailPaddingEnumerable(paddingCount)) { + yield return solution; + } + + paddingCount++; + } + } + + private IEnumerable SolveAutoFailPaddingEnumerable(int paddingCount) { + List[] allSolutions = new List[problemAreas.Count]; + + int minimalLeft = 0; + for (var i = 0; i < problemAreas.Count; i++) { + var problemAreaSolution = + SolveSingleProblemAreaPadding(problemAreas[i], paddingCount, minimalLeft); + + if (problemAreaSolution.Count == 0 || problemAreaSolution.Last() < minimalLeft) { + yield break; + } + + allSolutions[i] = problemAreaSolution; + minimalLeft = problemAreaSolution.First(); + } + + // Remove impossible max padding + int maximalLeft = paddingCount; + for (int i = allSolutions.Length - 1; i >= 0; i--) { + allSolutions[i].RemoveAll(o => o > maximalLeft); + maximalLeft = allSolutions[i].Last(); + } + + foreach (var leftPadding in EnumerateSolutions(allSolutions)) { + int[] pads = new int[leftPadding.Length + 1]; + int left = 0; + for (int i = 0; i < leftPadding.Length; i++) { + pads[i] = leftPadding[i] - left; + left = leftPadding[i]; + } + + pads[pads.Length - 1] = paddingCount - left; + yield return pads; + } + } + + private IEnumerable EnumerateSolutions(IReadOnlyList> allSolutions, int depth = 0, int minimum = 0) { + if (depth == allSolutions.Count - 1) { + foreach (var i in allSolutions[depth].Where(o => o >= minimum)) { + var s = new int[allSolutions.Count]; + s[depth] = i; + yield return s; + } + yield break; + } + foreach (var i in allSolutions[depth].Where(o => o >= minimum)) { + foreach (var j in EnumerateSolutions(allSolutions, depth + 1, minimum = i)) { + j[depth] = i; + yield return j; + } + } + } + + private List SolveSingleProblemAreaPadding(ProblemArea problemArea, int paddingCount, int minimalLeft = 0) { + var solution = new List(paddingCount - minimalLeft + 1); + + for (int left = minimalLeft; left <= paddingCount; left++) { + var right = paddingCount - left; + + if (ProblemAreaPaddingWorks(problemArea, left, right)) { + solution.Add(left); + } + } + + return solution; + } + + private bool ProblemAreaPaddingWorks(ProblemArea problemArea, int left, int right) { + foreach (var time in problemArea.timesToCheck) { + var minimalLeft = time - approachTime; + var minimalRight = time + approachTime; + + var startIndex = PaddedOsuBinarySearch(minimalLeft, left, right); + var endIndex = hitObjects.FindIndex(startIndex, ho => ho.Time > minimalRight); + if (endIndex < 0) { + endIndex = hitObjects.Count - 1; + } + + if (startIndex > problemArea.index || endIndex < problemArea.index || time > autoFailCheckTime) { + return false; + } + } + + return true; + } + + private int OsuBinarySearch(int time) { + var n = hitObjects.Count; + var min = 0; + var max = n - 1; + while (min <= max) { + var mid = min + (max - min) / 2; + var t = (int)hitObjects[mid].EndTime; + + if (time == t) { + return mid; + } + if (time > t) { + min = mid + 1; + } else { + max = mid - 1; + } + } + + return min; + } + + private int PaddedOsuBinarySearch(int time, int left, int right) { + var n = hitObjects.Count; + var min = -left; + var max = n - 1 + right; + while (min <= max) { + var mid = min + (max - min) / 2; + var t = mid < 0 ? int.MinValue : mid > hitObjects.Count - 1 ? int.MaxValue : (int)hitObjects[mid].EndTime; + + if (time == t) { + return mid; + } + if (time > t) { + min = mid + 1; + } else { + max = mid - 1; + } + } + + return min; + } + } +} \ No newline at end of file diff --git a/Mapping Tools/Classes/Tools/MapCleaner.cs b/Mapping Tools/Classes/Tools/MapCleaner.cs index e4d39e3c..b19f97d7 100644 --- a/Mapping Tools/Classes/Tools/MapCleaner.cs +++ b/Mapping Tools/Classes/Tools/MapCleaner.cs @@ -119,7 +119,7 @@ public static MapCleanerResult CleanMap(BeatmapEditor editor, MapCleanerArgs arg // Add redlines List redlines = timing.GetAllRedlines(); foreach (TimingPoint tp in redlines) { - timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true, meter: true, inherited: true, omitFirstBarLine: true)); + timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true, meter: true, unInherited: true, omitFirstBarLine: true)); } UpdateProgressBar(worker, 55); diff --git a/Mapping Tools/Classes/Tools/PatternGallery/CollectionRenameVm.cs b/Mapping Tools/Classes/Tools/PatternGallery/CollectionRenameVm.cs new file mode 100644 index 00000000..a1dc972d --- /dev/null +++ b/Mapping Tools/Classes/Tools/PatternGallery/CollectionRenameVm.cs @@ -0,0 +1,13 @@ +using System.ComponentModel; + +namespace Mapping_Tools.Classes.Tools.PatternGallery { + public class CollectionRenameVm { + [DisplayName("New name")] + [Description("The new name for the collection.")] + public string NewName { get; set; } + + [DisplayName("New directory name")] + [Description("The new name for the collection's directory in the Pattern Files directory.")] + public string NewFolderName { get; set; } + } +} \ No newline at end of file diff --git a/Mapping Tools/Classes/Tools/PatternGallery/OsuPattern.cs b/Mapping Tools/Classes/Tools/PatternGallery/OsuPattern.cs new file mode 100644 index 00000000..76e1299a --- /dev/null +++ b/Mapping Tools/Classes/Tools/PatternGallery/OsuPattern.cs @@ -0,0 +1,74 @@ +using Mapping_Tools.Classes.SystemTools; +using System; +using Mapping_Tools.Classes.BeatmapHelper; + +namespace Mapping_Tools.Classes.Tools.PatternGallery { + /// + /// Must store the objects, the greenlines, the timing, the global SV, the tickrate, the difficulty settings, + /// the hitsounds, absolute times and positions, combocolour index, combo numbers, stack leniency, gamemode. + /// Also store additional metadata such as the name, the date it was saved, use count, the map title, artist, diffname, and mapper. + /// + public class OsuPattern : BindableBase { + #region Fields + + private bool _isSelected; + public bool IsSelected { + get => _isSelected; + set => Set(ref _isSelected, value); + } + + private string _name; + public string Name { + get => _name; + set => Set(ref _name, value); + } + + private DateTime _creationTime; + public DateTime CreationTime { + get => _creationTime; + set => Set(ref _creationTime, value); + } + + private DateTime _lastUsedTime; + public DateTime LastUsedTime { + get => _lastUsedTime; + set => Set(ref _lastUsedTime, value); + } + + private int _useCount; + public int UseCount { + get => _useCount; + set => Set(ref _useCount, value); + } + + private string _fileName; + public string FileName { + get => _fileName; + set => Set(ref _fileName, value); + } + + private int _objectCount; + public int ObjectCount { + get => _objectCount; + set => Set(ref _objectCount, value); + } + + private TimeSpan _duration; + public TimeSpan Duration { + get => _duration; + set => Set(ref _duration, value); + } + + private double _beatLength; + public double BeatLength { + get => _beatLength; + set => Set(ref _beatLength, value); + } + + #endregion + + public Beatmap GetPatternBeatmap(OsuPatternFileHandler fileHandler) { + return new BeatmapEditor(fileHandler.GetPatternPath(FileName)).Beatmap; + } + } +} \ No newline at end of file diff --git a/Mapping Tools/Classes/Tools/PatternGallery/OsuPatternFileHandler.cs b/Mapping Tools/Classes/Tools/PatternGallery/OsuPatternFileHandler.cs new file mode 100644 index 00000000..7bd98dec --- /dev/null +++ b/Mapping Tools/Classes/Tools/PatternGallery/OsuPatternFileHandler.cs @@ -0,0 +1,65 @@ +using System; +using System.Data; +using System.IO; +using System.Linq; +using Mapping_Tools.Classes.MathUtil; +using Newtonsoft.Json; + +namespace Mapping_Tools.Classes.Tools.PatternGallery { + public class OsuPatternFileHandler { + [JsonIgnore] + public string BasePath { get; set; } + + public string CollectionFolderName { get; set; } + + public OsuPatternFileHandler() { + CollectionFolderName = RNG.RandomString(20); + } + + public OsuPatternFileHandler(string basePath) { + CollectionFolderName = RNG.RandomString(20); + BasePath = basePath; + EnsureCollectionFolderExists(); + } + + public void EnsureCollectionFolderExists() { + Directory.CreateDirectory(GetPatternFilesFolderPath()); + } + + public string GetCollectionFolderPath() { + return Path.Combine(BasePath, CollectionFolderName); + } + + public string GetPatternFilesFolderPath() { + return Path.Combine(GetCollectionFolderPath(), @"Pattern Files"); + } + + public string GetPatternPath(string fileName) { + return Path.Combine(GetPatternFilesFolderPath(), fileName); + } + + /// + /// Gets the names of all the collection folders in the pattern folder + /// + /// + public string[] GetCollectionFolderNames() { + return Directory.GetDirectories(BasePath) + .Select(Path.GetFileName).ToArray(); + } + + public bool CollectionFolderExists(string name) { + return GetCollectionFolderNames().Contains(name); + } + + public void RenameCollectionFolder(string newName) { + if (CollectionFolderName == newName) return; + + if (CollectionFolderExists(newName)) { + throw new DuplicateNameException($"A collection with the name {newName} already exists in {BasePath}."); + } + + Directory.Move(GetCollectionFolderPath(), Path.Combine(BasePath, newName)); + CollectionFolderName = newName; + } + } +} \ No newline at end of file diff --git a/Mapping Tools/Classes/Tools/PatternGallery/OsuPatternMaker.cs b/Mapping Tools/Classes/Tools/PatternGallery/OsuPatternMaker.cs new file mode 100644 index 00000000..0779dd09 --- /dev/null +++ b/Mapping Tools/Classes/Tools/PatternGallery/OsuPatternMaker.cs @@ -0,0 +1,85 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Mapping_Tools.Classes.BeatmapHelper; +using Mapping_Tools.Classes.MathUtil; + +namespace Mapping_Tools.Classes.Tools.PatternGallery { + public class OsuPatternMaker { + public double Padding { get; set; } = 5; + + public OsuPattern FromSelectedWithSave(Beatmap beatmap, string name, OsuPatternFileHandler fileHandler) { + var osuPattern = FromSelected(beatmap, name, out var patternBeatmap); + + // Could possibly be saved async + patternBeatmap.SaveWithFloatPrecision = true; + Editor.SaveFile(fileHandler.GetPatternPath(osuPattern.FileName), patternBeatmap.GetLines()); + + return osuPattern; + } + + public OsuPattern FromSelected(Beatmap beatmap, string name, out Beatmap patternBeatmap) { + // Copy it so the changes dont affect the given beatmap object + patternBeatmap = beatmap.DeepCopy(); + + // Remove the storyboarding + patternBeatmap.StoryboardLayerFail.Clear(); + patternBeatmap.StoryboardLayerPass.Clear(); + patternBeatmap.StoryboardLayerBackground.Clear(); + patternBeatmap.StoryboardLayerForeground.Clear(); + patternBeatmap.StoryboardLayerOverlay.Clear(); + + // Keep the selected subset of hit objects + patternBeatmap.HitObjects = patternBeatmap.HitObjects.Where(h => h.IsSelected).ToList(); + + var startTime = patternBeatmap.GetHitObjectStartTime() - Padding; + var endTime = patternBeatmap.GetHitObjectEndTime() + Padding; + + // Keep the timing points in the range of the hitobjects + patternBeatmap.BeatmapTiming.TimingPoints = patternBeatmap.BeatmapTiming.TimingPoints + .Where(tp => tp.Offset >= startTime && tp.Offset <= endTime).ToList(); + + // Add some earlier timing points if necessary + var firstUnInheritedTimingPoint = patternBeatmap.HitObjects.First().UnInheritedTimingPoint; + var firstNormalTimingPoint = patternBeatmap.HitObjects.First().TimingPoint; + + if (!patternBeatmap.BeatmapTiming.TimingPoints.Contains(firstUnInheritedTimingPoint)) { + patternBeatmap.BeatmapTiming.TimingPoints.Add(firstUnInheritedTimingPoint); + } + if (!patternBeatmap.BeatmapTiming.TimingPoints.Contains(firstNormalTimingPoint)) { + patternBeatmap.BeatmapTiming.TimingPoints.Add(firstNormalTimingPoint); + } + patternBeatmap.BeatmapTiming.Sort(); + + // Generate a file name and save the pattern + var now = DateTime.Now; + var fileName = GenerateUniquePatternFileName(name, now); + + return new OsuPattern { + Name = name, + CreationTime = now, + LastUsedTime = now, + FileName = fileName, + ObjectCount = patternBeatmap.HitObjects.Count, + Duration = TimeSpan.FromMilliseconds(endTime - startTime - 2 * Padding), + BeatLength = patternBeatmap.BeatmapTiming.GetBeatLength(startTime + Padding, endTime - Padding, true) + }; + } + + private static string GenerateUniquePatternFileName(string name, DateTime time) { + var fileName = time.ToString("yyyy-MM-dd HH-mm-ss") + "_" + RNG.RandomString(8) + "__" + name; + + if (!fileName.EndsWith(".osu")) { + fileName += ".osu"; + } + + // Remove invalid characters + string regexSearch = new string(Path.GetInvalidFileNameChars()); + Regex r = new Regex($"[{Regex.Escape(regexSearch)}]"); + fileName = r.Replace(fileName, ""); + + return fileName; + } + } +} \ No newline at end of file diff --git a/Mapping Tools/Classes/Tools/PatternGallery/OsuPatternPlacer.cs b/Mapping Tools/Classes/Tools/PatternGallery/OsuPatternPlacer.cs new file mode 100644 index 00000000..9b27072d --- /dev/null +++ b/Mapping Tools/Classes/Tools/PatternGallery/OsuPatternPlacer.cs @@ -0,0 +1,162 @@ +using Mapping_Tools.Classes.BeatmapHelper; +using System; +using System.Collections.Generic; +using System.Linq; +using Mapping_Tools.Classes.SystemTools; + +namespace Mapping_Tools.Classes.Tools.PatternGallery { + /// + /// Helper class for placing a into a . + /// + public class OsuPatternPlacer : BindableBase { + public double Padding = 5; + public double PartingDistance = 4; + public PatternOverwriteMode PatternOverwriteMode = PatternOverwriteMode.PartitionedOverwrite; + public TimingOverwriteMode TimingOverwriteMode = TimingOverwriteMode.InPatternRelativeTiming; + public bool IncludeHitsounds = true; + public bool ScaleToNewCircleSize = true; + public bool ScaleToNewTiming = true; + public bool SnapToNewTiming = true; + public int SnapDivisor1 = 16; + public int SnapDivisor2 = 12; + public bool FixGlobalSV = true; + public bool FixColourHax = true; + public bool FixStackLeniency = true; + public bool FixTickRate = true; + public double CustomScale = 1; + public double CustomRotate = 0; + + /// + /// 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. + /// + /// The pattern beatmap to be placed into the beatmap. + /// To beatmap to place the pattern in. + /// The time at which to place the first hit object of the pattern beatmap. + /// If true, copies the pattern beatmap to prevent the pattern beatmap from being modified by this method. + public void PlaceOsuPatternAtTime(Beatmap patternBeatmap, Beatmap beatmap, double time = double.NaN, bool protectBeatmapPattern = true) { + double offset = double.IsNaN(time) ? 0 : time - patternBeatmap.GetHitObjectStartTime(); + PlaceOsuPattern(patternBeatmap, beatmap, offset, protectBeatmapPattern); + } + + /// + /// 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. + /// + /// The pattern beatmap to be placed into the beatmap. + /// To beatmap to place the pattern in. + /// An offset to move the pattern beatmap in time with. + /// If true, copies the pattern beatmap to prevent the pattern beatmap from being modified by this method. + 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> parts; + if (PatternOverwriteMode == PatternOverwriteMode.PartitionedOverwrite) { + parts = PartitionBeatmap(patternBeatmap); + } + else { + parts = new List> { + new Tuple(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(); + 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(); + } + + /// + /// Creates parts that have at least PartingDistance number of beats of a gap between the parts. + /// + /// The beatmap to partition. + /// List of tuples with start time, end time. + private List> PartitionBeatmap(Beatmap beatmap) { + List> parts = new List>(); + + var firstTime = beatmap.HitObjects[0].Time; + var lastObject = beatmap.HitObjects[0]; + foreach (var ho in beatmap.HitObjects.Skip(1)) { + var gap = beatmap.BeatmapTiming.GetBeatLength(lastObject.EndTime, ho.Time); + + if (gap >= PartingDistance) { + parts.Add(new Tuple(firstTime, lastObject.EndTime)); + firstTime = ho.Time; + } + + lastObject = ho; + } + parts.Add(new Tuple(firstTime, lastObject.EndTime)); + + return parts; + } + + /// + /// Removes hitobjects and timingpoints in the beatmap between the start and the end time + /// + /// + /// + /// + private static void RemovePartOfBeatmap(Beatmap beatmap, double startTime, double endTime) { + beatmap.HitObjects.RemoveAll(h => h.Time >= startTime && h.Time <= endTime); + beatmap.BeatmapTiming.TimingPoints.RemoveAll(tp => tp.Offset >= startTime && tp.Offset <= endTime); + } + + private static TimingPointsChange GetTimingPointsChange(HitObject ho, bool sv, bool hs) { + var tp = ho.TimingPoint.Copy(); + tp.Offset = ho.Time; + tp.Uninherited = false; + tp.MpB = ho.SliderVelocity; + return new TimingPointsChange(tp, sv, false, hs, hs, hs); + } + + private static TimingPointsChange GetTimingPointsChange(TimingPoint tp, bool sv, bool hs) { + tp = tp.Copy(); + tp.MpB = tp.Uninherited ? -100 : tp.MpB; + tp.Uninherited = false; + return new TimingPointsChange(tp, sv, false, hs, hs, hs); + } + } +} \ No newline at end of file diff --git a/Mapping Tools/Classes/Tools/PatternGallery/PatternOverwriteMode.cs b/Mapping Tools/Classes/Tools/PatternGallery/PatternOverwriteMode.cs new file mode 100644 index 00000000..c322410f --- /dev/null +++ b/Mapping Tools/Classes/Tools/PatternGallery/PatternOverwriteMode.cs @@ -0,0 +1,16 @@ +namespace Mapping_Tools.Classes.Tools.PatternGallery { + public enum PatternOverwriteMode { + /// + /// Remove no objects from the original beatmap. + /// + NoOverwrite, + /// + /// Remove objects from the original beatmap only in dense parts of the pattern. + /// + PartitionedOverwrite, + /// + /// Remove all objects from the original beatmap between the start time of the pattern and the end time of the pattern. + /// + CompleteOverwrite, + } +} \ No newline at end of file diff --git a/Mapping Tools/Classes/Tools/PatternGallery/TimingOverwriteMode.cs b/Mapping Tools/Classes/Tools/PatternGallery/TimingOverwriteMode.cs new file mode 100644 index 00000000..133b1999 --- /dev/null +++ b/Mapping Tools/Classes/Tools/PatternGallery/TimingOverwriteMode.cs @@ -0,0 +1,8 @@ +namespace Mapping_Tools.Classes.Tools.PatternGallery { + public enum TimingOverwriteMode { + OriginalTimingOnly, + InPatternRelativeTiming, + InPatternAbsoluteTiming, + PatternTimingOnly + } +} \ No newline at end of file diff --git a/Mapping Tools/Classes/Tools/TimingPointsChange.cs b/Mapping Tools/Classes/Tools/TimingPointsChange.cs index b5d8c852..0e1044e8 100644 --- a/Mapping Tools/Classes/Tools/TimingPointsChange.cs +++ b/Mapping Tools/Classes/Tools/TimingPointsChange.cs @@ -14,19 +14,19 @@ public struct TimingPointsChange { public bool Sampleset; public bool Index; public bool Volume; - public bool Inherited; + public bool UnInherited; public bool Kiai; public bool OmitFirstBarLine; public double Fuzzyness; - public TimingPointsChange(TimingPoint tpNew, bool mpb = false, bool meter = false, bool sampleset = false, bool index = false, bool volume = false, bool inherited = false, bool kiai = false, bool omitFirstBarLine = false, double fuzzyness=2) { + public TimingPointsChange(TimingPoint tpNew, bool mpb = false, bool meter = false, bool sampleset = false, bool index = false, bool volume = false, bool unInherited = false, bool kiai = false, bool omitFirstBarLine = false, double fuzzyness=2) { MyTP = tpNew; MpB = mpb; Meter = meter; Sampleset = sampleset; Index = index; Volume = volume; - Inherited = inherited; + UnInherited = unInherited; Kiai = kiai; OmitFirstBarLine = omitFirstBarLine; Fuzzyness = fuzzyness; @@ -55,7 +55,7 @@ public void AddChange(List list, bool allAfter = false) { prevTimingPoint = onTimingPoints.Last(); } - if (Inherited && !onHasRed) { + if (UnInherited && !onHasRed) { // Make new redline if (prevTimingPoint == null) { addingTimingPoint = MyTP; @@ -66,7 +66,7 @@ public void AddChange(List list, bool allAfter = false) { } onTimingPoints.Add(addingTimingPoint); } - if (!Inherited && (onTimingPoints.Count == 0 || (MpB && !onHasGreen))) { + if (!UnInherited && (onTimingPoints.Count == 0 || (MpB && !onHasGreen))) { // Make new greenline (based on prev) if (prevTimingPoint == null) { addingTimingPoint = MyTP; @@ -80,16 +80,16 @@ public void AddChange(List list, bool allAfter = false) { } foreach (TimingPoint on in onTimingPoints) { - if (MpB && (Inherited ? on.Uninherited : !on.Uninherited)) { on.MpB = MyTP.MpB; } - if (Meter && Inherited && on.Uninherited) { on.Meter = MyTP.Meter; } + if (MpB && (UnInherited ? on.Uninherited : !on.Uninherited)) { on.MpB = MyTP.MpB; } + if (Meter && UnInherited && on.Uninherited) { on.Meter = MyTP.Meter; } if (Sampleset) { on.SampleSet = MyTP.SampleSet; } if (Index) { on.SampleIndex = MyTP.SampleIndex; } if (Volume) { on.Volume = MyTP.Volume; } if (Kiai) { on.Kiai = MyTP.Kiai; } - if (OmitFirstBarLine && Inherited && on.Uninherited) { on.OmitFirstBarLine = MyTP.OmitFirstBarLine; } + if (OmitFirstBarLine && UnInherited && on.Uninherited) { on.OmitFirstBarLine = MyTP.OmitFirstBarLine; } } - if (addingTimingPoint != null && (prevTimingPoint == null || !addingTimingPoint.SameEffect(prevTimingPoint) || Inherited)) { + if (addingTimingPoint != null && (prevTimingPoint == null || !addingTimingPoint.SameEffect(prevTimingPoint) || UnInherited)) { list.Add(addingTimingPoint); } @@ -106,8 +106,8 @@ public void AddChange(List list, bool allAfter = false) { } } - public static void ApplyChanges(Timing timing, List timingPointsChanges, bool allAfter = false) { - timingPointsChanges = timingPointsChanges.OrderBy(o => o.MyTP.Offset).ToList(); + public static void ApplyChanges(Timing timing, IEnumerable timingPointsChanges, bool allAfter = false) { + timingPointsChanges = timingPointsChanges.OrderBy(o => o.MyTP.Offset); foreach (TimingPointsChange c in timingPointsChanges) { c.AddChange(timing.TimingPoints, allAfter); } @@ -116,7 +116,7 @@ public static void ApplyChanges(Timing timing, List timingPo public void Debug() { Console.WriteLine(MyTP.GetLine()); - Console.WriteLine($"{MpB}, {Meter}, {Sampleset}, {Index}, {Volume}, {Inherited}, {Kiai}, {OmitFirstBarLine}"); + Console.WriteLine($"{MpB}, {Meter}, {Sampleset}, {Index}, {Volume}, {UnInherited}, {Kiai}, {OmitFirstBarLine}"); } public void AddChangeOld(List list, bool allAfter=false) { @@ -148,7 +148,7 @@ public void AddChangeOld(List list, bool allAfter=false) { if (Sampleset) { on.SampleSet = MyTP.SampleSet; } if (Index) { on.SampleIndex = MyTP.SampleIndex; } if (Volume) { on.Volume = MyTP.Volume; } - if (Inherited) { on.Uninherited = MyTP.Uninherited; } + if (UnInherited) { on.Uninherited = MyTP.Uninherited; } if (Kiai) { on.Kiai = MyTP.Kiai; } if (OmitFirstBarLine) { on.OmitFirstBarLine = MyTP.OmitFirstBarLine; } } else { @@ -164,11 +164,11 @@ public void AddChangeOld(List list, bool allAfter=false) { if (Sampleset) { on.SampleSet = MyTP.SampleSet; } if (Index) { on.SampleIndex = MyTP.SampleIndex; } if (Volume) { on.Volume = MyTP.Volume; } - if (Inherited) { on.Uninherited = MyTP.Uninherited; } + if (UnInherited) { on.Uninherited = MyTP.Uninherited; } if (Kiai) { on.Kiai = MyTP.Kiai; } if (OmitFirstBarLine) { on.OmitFirstBarLine = MyTP.OmitFirstBarLine; } - if (!on.SameEffect(prev) || Inherited) { + if (!on.SameEffect(prev) || UnInherited) { list.Add(on); } } else { diff --git a/Mapping Tools/Components/Dialogs/CustomDialog/CustomDialog.xaml b/Mapping Tools/Components/Dialogs/CustomDialog/CustomDialog.xaml new file mode 100644 index 00000000..1cad6f06 --- /dev/null +++ b/Mapping Tools/Components/Dialogs/CustomDialog/CustomDialog.xaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/Mapping Tools/Components/Dialogs/CustomDialog/CustomDialog.xaml.cs b/Mapping Tools/Components/Dialogs/CustomDialog/CustomDialog.xaml.cs new file mode 100644 index 00000000..10628889 --- /dev/null +++ b/Mapping Tools/Components/Dialogs/CustomDialog/CustomDialog.xaml.cs @@ -0,0 +1,157 @@ +using Mapping_Tools.Components.Domain; +using MaterialDesignThemes.Wpf; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Input; + +namespace Mapping_Tools.Components.Dialogs.CustomDialog { + /// + /// Interaction logic for OsuPatternImportDialog.xaml + /// + public partial class CustomDialog { + private readonly int _autoSelectIndex; + private int _populationIndex; + private UIElement _autoSelectElement; + + public CustomDialog(object viewModel, int autoSelectIndex = -1) { + if (viewModel == null) return; + + InitializeComponent(); + + DataContext = viewModel; + _autoSelectIndex = autoSelectIndex; + PopulateSettings(DataContext); + + AcceptButton.Command = new CommandImplementation(AcceptButtonCommand); + } + + private void AcceptButtonCommand(object parameter) { + // Remove logical focus to trigger LostFocus on any fields that didn't yet update the ViewModel + FocusManager.SetFocusedElement(FocusManager.GetFocusScope(this), null); + + DialogHost.CloseDialogCommand.Execute(parameter, this); + } + + private void PopulateSettings(object settings) { + _populationIndex = 0; + + var type = settings.GetType(); + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + AddPropertyControls(properties, settings); + } + + private void AddPropertyControls(IReadOnlyCollection props, object settings, bool useCard = false) { + if (props.Count == 0) return; + + if (useCard) { + var card = new Card {Margin = new Thickness(10)}; + var cardPanel = new StackPanel(); + card.Content = cardPanel; + + foreach (var prop in props) { + var e = GetSettingControl(prop, settings); + if (e != null) { + cardPanel.Children.Add(e); + } + } + + Panel.Children.Add(card); + } + else { + foreach (var prop in props) { + var e = GetSettingControl(prop, settings); + if (e != null) { + Panel.Children.Add(e); + } + } + } + } + + private UIElement GetSettingControl(PropertyInfo prop, object settings) { + if (!prop.CanWrite || !prop.CanRead) return null; + + var value = prop.GetValue(settings); + if (value == null) return null; + + string name; + if (prop.GetCustomAttribute(typeof(DisplayNameAttribute)) is DisplayNameAttribute n) { + name = n.DisplayName; + } else { + name = prop.Name; + } + + string description = null; + if (prop.GetCustomAttribute(typeof(DescriptionAttribute)) is DescriptionAttribute d) { + description = d.Description; + } + + UIElement content = null; + switch (value) { + case bool _: + var checkBox = new CheckBox { + Content = name, + ToolTip = description, + Margin = new Thickness(0, 0, 0, 5) + }; + + Binding toggleBinding = new Binding(prop.Name) { + Source = settings + }; + checkBox.SetBinding(ToggleButton.IsCheckedProperty, toggleBinding); + content = checkBox; + break; + case double _: + var doubleTextBox = new TextBox { + MinWidth = 100, + ToolTip = description, + Margin = new Thickness(0, 0, 0, 5), + Style = Application.Current.FindResource("MaterialDesignFloatingHintTextBox") as Style + }; + HintAssist.SetHint(doubleTextBox, name); + + Binding doubleBinding = new Binding(prop.Name) { + Source = settings, + Converter = new DoubleToStringConverter() + }; + doubleTextBox.SetBinding(TextBox.TextProperty, doubleBinding); + content = doubleTextBox; + break; + case string _: + var stringTextBox = new TextBox { + MinWidth = 100, + ToolTip = description, + Margin = new Thickness(0, 0, 0, 5), + Style = Application.Current.FindResource("MaterialDesignFloatingHintTextBox") as Style }; + HintAssist.SetHint(stringTextBox, name); + + Binding stringBinding = new Binding(prop.Name) { + Source = settings + }; + stringTextBox.SetBinding(TextBox.TextProperty, stringBinding); + content = stringTextBox; + break; + } + + if (content != null && _autoSelectIndex == _populationIndex) { + _autoSelectElement = content; + } + _populationIndex++; + + return content; + } + + private void CustomDialog_OnLoaded(object sender, RoutedEventArgs e) { + if (_autoSelectElement != null) { + _autoSelectElement.Focus(); + if (_autoSelectElement is TextBox textBox) { + textBox.SelectAll(); + } + } + } + } +} diff --git a/Mapping Tools/Components/TimeLine/TimeLineElement.xaml.cs b/Mapping Tools/Components/TimeLine/TimeLineElement.xaml.cs index 8bba992b..934ad65e 100644 --- a/Mapping Tools/Components/TimeLine/TimeLineElement.xaml.cs +++ b/Mapping Tools/Components/TimeLine/TimeLineElement.xaml.cs @@ -64,6 +64,17 @@ private void SetupColour() { Color = Color.FromRgb(252, 66, 20) }; break; + case 4: + // Create a Brush + Inner = new SolidColorBrush { + Color = Color.FromRgb(145, 37, 175) + }; + + // Create a Brush + Outer = new SolidColorBrush { + Color = Color.FromRgb(172, 56, 190) + }; + break; default: // Create a Brush Inner = new SolidColorBrush { diff --git a/Mapping Tools/MainWindow.xaml.cs b/Mapping Tools/MainWindow.xaml.cs index 0d27d64b..0f8a990e 100644 --- a/Mapping Tools/MainWindow.xaml.cs +++ b/Mapping Tools/MainWindow.xaml.cs @@ -30,7 +30,6 @@ public partial class MainWindow { public static string AppCommon { get; set; } public static string AppDataPath { get; set; } public static string ExportPath { get; set; } - public static Random MainRandom { get; set; } public static HttpClient HttpClient { get; set; } public static SnackbarMessageQueue MessageQueue { get; set; } @@ -43,7 +42,6 @@ public MainWindow() { AppCommon = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); AppDataPath = Path.Combine(AppCommon, "Mapping Tools"); ExportPath = Path.Combine(AppDataPath, "Exports"); - MainRandom = new Random(); HttpClient = new HttpClient(); InitializeComponent(); diff --git a/Mapping Tools/Mapping_Tools.csproj b/Mapping Tools/Mapping_Tools.csproj index 384584ca..9a3b95f5 100644 --- a/Mapping Tools/Mapping_Tools.csproj +++ b/Mapping Tools/Mapping_Tools.csproj @@ -391,17 +391,28 @@ + + + + + + + + MessageDialog.xaml + + CustomDialog.xaml + @@ -513,6 +524,8 @@ + + @@ -539,6 +552,12 @@ ComboColourStudioView.xaml + + AutoFailDetectorView.xaml + + + PatternGalleryView.xaml + HitsoundStudioExportDialog.xaml @@ -614,6 +633,10 @@ MSBuild:Compile Designer + + MSBuild:Compile + Designer + MSBuild:Compile Designer @@ -709,6 +732,14 @@ MSBuild:Compile Designer + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + MSBuild:Compile Designer diff --git a/Mapping Tools/Views/AutoFailDetector/AutoFailDetectorView.xaml b/Mapping Tools/Views/AutoFailDetector/AutoFailDetectorView.xaml new file mode 100644 index 00000000..2f0f9d75 --- /dev/null +++ b/Mapping Tools/Views/AutoFailDetector/AutoFailDetectorView.xaml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + Detects cases of incorrect object loading in a beatmap which makes osu! unable to calculate scores correctly. + + Auto-fail is most often caused by placing other hit objects during sliders, so there are multiple hit objects going on at the same time also known as "2B" patterns. + + Use the AR and OD override options to see what would happen when you use hardrock mod on the map. + + + + + + + + This tool is compatible with QuickRun! + + + + + + Show unloading hit objects + + + Show potential unloading hit objects + + + Show disrupting hit objects + + + Get auto-fail fix guide + + + Auto-insert spinners + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mapping Tools/Views/AutoFailDetector/AutoFailDetectorView.xaml.cs b/Mapping Tools/Views/AutoFailDetector/AutoFailDetectorView.xaml.cs new file mode 100644 index 00000000..942d3301 --- /dev/null +++ b/Mapping Tools/Views/AutoFailDetector/AutoFailDetectorView.xaml.cs @@ -0,0 +1,176 @@ +using Mapping_Tools.Classes.BeatmapHelper; +using Mapping_Tools.Classes.SystemTools; +using Mapping_Tools.Classes.SystemTools.QuickRun; +using Mapping_Tools.Classes.Tools; +using Mapping_Tools.Components.TimeLine; +using Mapping_Tools.Viewmodels; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Windows; +using System.Windows.Input; + +namespace Mapping_Tools.Views.AutoFailDetector { + [SmartQuickRunUsage(SmartQuickRunTargets.Always)] + public partial class AutoFailDetectorView : IQuickRun { + private List _unloadingObjects = new List(); + private List _potentialUnloadingObjects = new List(); + private List _potentialDisruptors = new List(); + private double _endTimeMonitor; + private TimeLine _tl; + + /// + /// + /// + public event EventHandler RunFinished; + + /// + /// + /// + public static readonly string ToolName = "Auto-fail Detector"; + + /// + /// + /// + public static readonly string ToolDescription = $"Detects cases of incorrect object loading in a beatmap which makes osu! unable to calculate scores correctly.{Environment.NewLine} Auto-fail is most often caused by placing other hit objects during sliders, so there are multiple hit objects going on at the same time also known as \"2B\" patterns.{Environment.NewLine} Use the AR and OD override options to see what would happen when you use hardrock mod on the map."; + + /// + /// Initializes the Map Cleaner view to + /// + public AutoFailDetectorView() { + InitializeComponent(); + Width = MainWindow.AppWindow.content_views.Width; + Height = MainWindow.AppWindow.content_views.Height; + DataContext = new AutoFailDetectorVm(); + + // It's important to see the results + Verbose = true; + } + + public AutoFailDetectorVm ViewModel => (AutoFailDetectorVm) DataContext; + + private void Start_Click(object sender, RoutedEventArgs e) { + RunTool(MainWindow.AppWindow.GetCurrentMaps(), quick: false); + } + + /// + /// + /// + public void QuickRun() { + RunTool(new[] { IOHelper.GetCurrentBeatmapOrCurrentBeatmap() }, quick: true); + } + + private void RunTool(string[] paths, bool quick = false) { + if (!CanRun) return; + + // Remove logical focus to trigger LostFocus on any fields that didn't yet update the ViewModel + FocusManager.SetFocusedElement(FocusManager.GetFocusScope(this), null); + + //BackupManager.SaveMapBackup(paths); + + ViewModel.Paths = paths; + ViewModel.Quick = quick; + + BackgroundWorker.RunWorkerAsync(ViewModel); + CanRun = false; + } + + protected override void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e) { + var bgw = sender as BackgroundWorker; + e.Result = Run_Program((AutoFailDetectorVm) e.Argument, bgw, e); + } + + protected override void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { + if (e.Error == null) { + FillTimeLine(); + } + base.BackgroundWorker_RunWorkerCompleted(sender, e); + } + + private string Run_Program(AutoFailDetectorVm args, BackgroundWorker worker, DoWorkEventArgs _) { + var reader = EditorReaderStuff.GetFullEditorReaderOrNot(); + var editor = EditorReaderStuff.GetNewestVersionOrNot(args.Paths[0], reader); + var beatmap = editor.Beatmap; + + // Get approach time and radius of the 50 score hit window + var ar = args.ApproachRateOverride == -1 + ? editor.Beatmap.Difficulty["ApproachRate"].DoubleValue + : args.ApproachRateOverride; + var approachTime = (int) Beatmap.ApproachRateToMs(ar); + + var od = args.OverallDifficultyOverride == -1 + ? editor.Beatmap.Difficulty["OverallDifficulty"].DoubleValue + : args.OverallDifficultyOverride; + var window50 = (int) Math.Ceiling(200 - 10 * od); + + // Start time and end time + var mapStartTime = (int) beatmap.GetMapStartTime(); + var mapEndTime = (int) beatmap.GetMapEndTime(); + var autoFailTime = (int) beatmap.GetAutoFailCheckTime(); + + // Detect auto-fail + var autoFailDetector = new Classes.Tools.AutoFailDetector(beatmap.HitObjects, + mapStartTime, mapEndTime, autoFailTime, + approachTime, window50, args.PhysicsUpdateLeniency); + + var autoFail = autoFailDetector.DetectAutoFail(); + + if (worker != null && worker.WorkerReportsProgress) worker.ReportProgress(33); + + // Fix auto-fail + if (args.GetAutoFailFix) { + var placedFix = autoFailDetector.AutoFailFixDialogue(args.AutoPlaceFix); + + if (placedFix) { + editor.SaveFile(); + } + } + + if (worker != null && worker.WorkerReportsProgress) worker.ReportProgress(67); + + // Set the timeline lists + if (args.ShowUnloadingObjects) + _unloadingObjects = autoFailDetector.UnloadingObjects; + if (args.ShowPotentialUnloadingObjects) + _potentialUnloadingObjects = autoFailDetector.PotentialUnloadingObjects; + if (args.ShowPotentialDisruptors) + _potentialDisruptors = autoFailDetector.Disruptors; + + // Set end time for the timeline + _endTimeMonitor = mapEndTime; + + // Complete progressbar + if (worker != null && worker.WorkerReportsProgress) worker.ReportProgress(100); + + // Do stuff + if (args.Quick) + RunFinished?.Invoke(this, new RunToolCompletedEventArgs(true, false)); + + return autoFail ? $"{autoFailDetector.UnloadingObjects.Count} unloading objects detected and {autoFailDetector.PotentialUnloadingObjects.Count} potential unloading objects detected!" : + autoFailDetector.PotentialUnloadingObjects.Count > 0 ? $"No auto-fail, but {autoFailDetector.PotentialUnloadingObjects.Count} potential unloading objects detected." : + "No auto-fail detected."; + } + + + private void FillTimeLine() { + _tl?.mainCanvas.Children.Clear(); + try { + _tl = new TimeLine(MainWindow.AppWindow.ActualWidth, 100.0, _endTimeMonitor); + foreach (double timingS in _potentialUnloadingObjects) { + _tl.AddElement(timingS, 1); + } + foreach (double timingS in _potentialDisruptors) { + _tl.AddElement(timingS, 4); + } + foreach (double timingS in _unloadingObjects) { + _tl.AddElement(timingS, 3); + } + tl_host.Children.Clear(); + tl_host.Children.Add(_tl); + } catch (Exception ex) { + Console.WriteLine(ex.Message); + } + } + + } +} diff --git a/Mapping Tools/Views/PatternGallery/PatternGalleryView.xaml b/Mapping Tools/Views/PatternGallery/PatternGalleryView.xaml new file mode 100644 index 00000000..f5906f2a --- /dev/null +++ b/Mapping Tools/Views/PatternGallery/PatternGalleryView.xaml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Patterns + + + + + + + + This tool is compatible with QuickRun! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mapping Tools/Views/PatternGallery/PatternGalleryView.xaml.cs b/Mapping Tools/Views/PatternGallery/PatternGalleryView.xaml.cs new file mode 100644 index 00000000..b8a96019 --- /dev/null +++ b/Mapping Tools/Views/PatternGallery/PatternGalleryView.xaml.cs @@ -0,0 +1,166 @@ +using Mapping_Tools.Classes.SystemTools; +using Mapping_Tools.Classes.SystemTools.QuickRun; +using Mapping_Tools.Classes.Tools; +using Mapping_Tools.Classes.Tools.PatternGallery; +using Mapping_Tools.Viewmodels; +using System; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Mapping_Tools.Classes; +using Mapping_Tools.Components.Dialogs.CustomDialog; +using MaterialDesignThemes.Wpf; + +namespace Mapping_Tools.Views.PatternGallery { + /// + /// Interactielogica voor PatternGalleryView.xaml + /// + [SmartQuickRunUsage(SmartQuickRunTargets.Always)] + [HiddenTool] + public partial class PatternGalleryView : ISavable, IQuickRun, IHaveExtraProjectMenuItems { + public string AutoSavePath => Path.Combine(MainWindow.AppDataPath, "patterngalleryproject.json"); + + public string DefaultSaveFolder => Path.Combine(MainWindow.AppDataPath, "Pattern Gallery Projects"); + + public static readonly string ToolName = "Pattern Gallery"; + public static readonly string ToolDescription = + $@"Save and load patterns from osu! beatmaps."; + + /// + /// + /// + public PatternGalleryView() + { + InitializeComponent(); + DataContext = new PatternGalleryVm(); + Width = MainWindow.AppWindow.content_views.Width; + Height = MainWindow.AppWindow.content_views.Height; + ProjectManager.LoadProject(this, message: false); + InitializeOsuPatternFileHandler(); + } + + public PatternGalleryVm ViewModel => (PatternGalleryVm)DataContext; + + public event EventHandler RunFinished; + + protected override void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e) + { + var bgw = sender as BackgroundWorker; + e.Result = ExportPattern((PatternGalleryVm) e.Argument, bgw, e); + } + + private void Start_Click(object sender, RoutedEventArgs e) + { + RunTool(MainWindow.AppWindow.GetCurrentMaps(), quick: false); + } + + public void QuickRun() + { + RunTool(new[] {IOHelper.GetCurrentBeatmapOrCurrentBeatmap()}, quick: true); + } + + private void RunTool(string[] paths, bool quick = false) + { + if (!CanRun) return; + + // Remove logical focus to trigger LostFocus on any fields that didn't yet update the ViewModel + FocusManager.SetFocusedElement(FocusManager.GetFocusScope(this), null); + + BackupManager.SaveMapBackup(paths); + + ViewModel.Paths = paths; + ViewModel.Quick = quick; + + BackgroundWorker.RunWorkerAsync(DataContext); + + CanRun = false; + } + + private string ExportPattern(PatternGalleryVm args, BackgroundWorker worker, DoWorkEventArgs _) { + var reader = EditorReaderStuff.GetFullEditorReaderOrNot(); + var editor = EditorReaderStuff.GetNewestVersionOrNot(IOHelper.GetCurrentBeatmapOrCurrentBeatmap(), reader); + + var pattern = args.Patterns.FirstOrDefault(o => o.IsSelected); + if (pattern == null) + throw new Exception("No pattern has been selected to export."); + + var patternBeatmap = pattern.GetPatternBeatmap(args.FileHandler); + + var patternPlacer = args.OsuPatternPlacer; + if (reader != null) { + patternPlacer.PlaceOsuPatternAtTime(patternBeatmap, editor.Beatmap, reader.EditorTime(), false); + } else { + patternPlacer.PlaceOsuPattern(patternBeatmap, editor.Beatmap, protectBeatmapPattern:false); + } + + editor.SaveFile(); + + // Increase pattern use count and time + pattern.UseCount++; + pattern.LastUsedTime = DateTime.Now; + + // Complete progressbar + if (worker != null && worker.WorkerReportsProgress) worker.ReportProgress(100); + + // Do stuff + if (args.Quick) + RunFinished?.Invoke(this, new RunToolCompletedEventArgs(true, reader != null)); + + return "Successfully exported pattern!"; + } + + private void InitializeOsuPatternFileHandler() { + // Make sure the file handler always uses the right pattern files folder + if (ViewModel.FileHandler != null) { + ViewModel.FileHandler.BasePath = DefaultSaveFolder; + ViewModel.FileHandler.EnsureCollectionFolderExists(); + } + } + + public PatternGalleryVm GetSaveData() + { + return (PatternGalleryVm) DataContext; + } + + public void SetSaveData(PatternGalleryVm saveData) + { + DataContext = saveData; + InitializeOsuPatternFileHandler(); + } + + public MenuItem[] GetMenuItems() { + var menu = new MenuItem { + Header = "_Rename collection", Icon = new PackIcon { Kind = PackIconKind.Rename }, + ToolTip = "Rename this collection and the collection's directory in the Pattern Files directory." + }; + menu.Click += DoRenameCollection; + + return new[] { menu }; + } + + private async void DoRenameCollection(object sender, RoutedEventArgs e) { + try { + var viewModel = new CollectionRenameVm { + NewName = ViewModel.CollectionName, + NewFolderName = ViewModel.FileHandler.CollectionFolderName + }; + + var dialog = new CustomDialog(viewModel, 0); + var result = await DialogHost.Show(dialog, "RootDialog"); + + if (!(bool)result) return; + + ViewModel.CollectionName = viewModel.NewName; + ViewModel.FileHandler.RenameCollectionFolder(viewModel.NewFolderName); + + await Task.Factory.StartNew(() => MainWindow.MessageQueue.Enqueue("Successfully renamed this collection!")); + } catch (ArgumentException) { } catch (Exception ex) { + ex.Show(); + } + } + } +} diff --git a/Mapping Tools/viewmodels/AutoFailDetectorVm.cs b/Mapping Tools/viewmodels/AutoFailDetectorVm.cs new file mode 100644 index 00000000..832a8009 --- /dev/null +++ b/Mapping Tools/viewmodels/AutoFailDetectorVm.cs @@ -0,0 +1,68 @@ +using Mapping_Tools.Classes.SystemTools; +using Mapping_Tools.Classes.Tools; +using Newtonsoft.Json; + +namespace Mapping_Tools.Viewmodels { + /// + /// Auto-fail detector View Model + /// + public class AutoFailDetectorVm : BindableBase { + [JsonIgnore] + public string[] Paths { get; set; } + + [JsonIgnore] + public bool Quick { get; set; } + + private bool _showUnloadingObjects = true; + public bool ShowUnloadingObjects { + get => _showUnloadingObjects; + set => Set(ref _showUnloadingObjects, value); + } + + private bool _showPotentialUnloadingObjects; + public bool ShowPotentialUnloadingObjects { + get => _showPotentialUnloadingObjects; + set => Set(ref _showPotentialUnloadingObjects, value); + } + + private bool _showPotentialDisruptors; + public bool ShowPotentialDisruptors { + get => _showPotentialDisruptors; + set => Set(ref _showPotentialDisruptors, value); + } + + private double _approachRateOverride = -1; + public double ApproachRateOverride { + get => _approachRateOverride; + set => Set(ref _approachRateOverride, value); + } + + private double _overallDifficultyOverride = -1; + public double OverallDifficultyOverride { + get => _overallDifficultyOverride; + set => Set(ref _overallDifficultyOverride, value); + } + + private int _physicsUpdateLeniency = 9; + public int PhysicsUpdateLeniency { + get => _physicsUpdateLeniency; + set => Set(ref _physicsUpdateLeniency, value); + } + + private bool _getAutoFailFix; + public bool GetAutoFailFix { + get => _getAutoFailFix; + set => Set(ref _getAutoFailFix, value); + } + + private bool _autoPlaceFix; + public bool AutoPlaceFix { + get => _autoPlaceFix; + set => Set(ref _autoPlaceFix, value); + } + + public AutoFailDetectorVm() { + + } + } +} diff --git a/Mapping Tools/viewmodels/PatternGalleryVm.cs b/Mapping Tools/viewmodels/PatternGalleryVm.cs new file mode 100644 index 00000000..caac44a0 --- /dev/null +++ b/Mapping Tools/viewmodels/PatternGalleryVm.cs @@ -0,0 +1,172 @@ +using Mapping_Tools.Classes; +using Mapping_Tools.Classes.SystemTools; +using Mapping_Tools.Classes.Tools; +using Mapping_Tools.Classes.Tools.PatternGallery; +using Mapping_Tools.Components.Domain; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Mapping_Tools.Viewmodels { + public class PatternGalleryVm : BindableBase { + private string _collectionName; + public string CollectionName { + get => _collectionName; + set => Set(ref _collectionName, value); + } + + private ObservableCollection _patterns; + public ObservableCollection Patterns { + get => _patterns; + set => Set(ref _patterns, value); + } + + public OsuPatternFileHandler FileHandler { get; set; } + + private bool? _isAllItemsSelected; + public bool? IsAllItemsSelected { + get => _isAllItemsSelected; + set { + if (Set(ref _isAllItemsSelected, value)) { + if (_isAllItemsSelected.HasValue) + SelectAll(_isAllItemsSelected.Value, Patterns); + } + } + } + + #region Export Options + + [JsonIgnore] + public OsuPatternPlacer OsuPatternPlacer { get; set; } + + /// + /// Extra time in millseconds around the patterns for deleting parts of the original map. + /// + public double Padding { + get => OsuPatternPlacer.Padding; + set => Set(ref OsuPatternPlacer.Padding, value); + } + + /// + /// Minimum time in beats necessary to separate parts of the pattern. + /// + public double PartingDistance { + get => OsuPatternPlacer.PartingDistance; + set => Set(ref OsuPatternPlacer.PartingDistance, value); + } + + public PatternOverwriteMode PatternOverwriteMode { + get => OsuPatternPlacer.PatternOverwriteMode; + set => Set(ref OsuPatternPlacer.PatternOverwriteMode, value); + } + + public TimingOverwriteMode TimingOverwriteMode { + get => OsuPatternPlacer.TimingOverwriteMode; + set => Set(ref OsuPatternPlacer.TimingOverwriteMode, value); + } + + public bool IncludeHitsounds { + get => OsuPatternPlacer.IncludeHitsounds; + set => Set(ref OsuPatternPlacer.IncludeHitsounds, value); + } + + public bool ScaleToNewCircleSize { + get => OsuPatternPlacer.ScaleToNewCircleSize; + set => Set(ref OsuPatternPlacer.ScaleToNewCircleSize, value); + } + + public bool ScaleToNewTiming { + get => OsuPatternPlacer.ScaleToNewTiming; + set => Set(ref OsuPatternPlacer.ScaleToNewTiming, value); + } + + public bool SnapToNewTiming { + get => OsuPatternPlacer.SnapToNewTiming; + set => Set(ref OsuPatternPlacer.SnapToNewTiming, value); + } + + public int SnapDivisor1 { + get => OsuPatternPlacer.SnapDivisor1; + set => Set(ref OsuPatternPlacer.SnapDivisor1, value); + } + + public int SnapDivisor2 { + get => OsuPatternPlacer.SnapDivisor2; + set => Set(ref OsuPatternPlacer.SnapDivisor2, value); + } + + public bool FixGlobalSV { + get => OsuPatternPlacer.FixGlobalSV; + set => Set(ref OsuPatternPlacer.FixGlobalSV, value); + } + + public bool FixColourHax { + get => OsuPatternPlacer.FixColourHax; + set => Set(ref OsuPatternPlacer.FixColourHax, value); + } + + public bool FixStackLeniency { + get => OsuPatternPlacer.FixStackLeniency; + set => Set(ref OsuPatternPlacer.FixStackLeniency, value); + } + + public bool FixTickRate { + get => OsuPatternPlacer.FixTickRate; + set => Set(ref OsuPatternPlacer.FixTickRate, value); + } + + public double CustomScale { + get => OsuPatternPlacer.CustomScale; + set => Set(ref OsuPatternPlacer.CustomScale, value); + } + + public double CustomRotate { + get => OsuPatternPlacer.CustomRotate; + set => Set(ref OsuPatternPlacer.CustomRotate, value); + } + + #endregion + + [JsonIgnore] + public CommandImplementation AddCommand { get; } + [JsonIgnore] + public CommandImplementation RemoveCommand { get; } + + + [JsonIgnore] + public string[] Paths { get; set; } + [JsonIgnore] + public bool Quick { get; set; } + + public PatternGalleryVm() { + CollectionName = @"My Pattern Collection"; + _patterns = new ObservableCollection(); + FileHandler = new OsuPatternFileHandler(); + OsuPatternPlacer = new OsuPatternPlacer(); + + AddCommand = new CommandImplementation( + _ => { + try { + var reader = EditorReaderStuff.GetFullEditorReader(); + var editor = EditorReaderStuff.GetNewestVersion(IOHelper.GetCurrentBeatmap(), reader); + var patternMaker = new OsuPatternMaker(); + var pattern = patternMaker.FromSelectedWithSave(editor.Beatmap, "test", FileHandler); + Patterns.Add(pattern); + } catch (Exception ex) { ex.Show(); } + }); + RemoveCommand = new CommandImplementation( + _ => { + try { + Patterns.RemoveAll(o => o.IsSelected); + } catch (Exception ex) { ex.Show(); } + }); + } + + private static void SelectAll(bool select, IEnumerable patterns) { + foreach (var model in patterns) { + model.IsSelected = select; + } + } + } +} diff --git a/Mapping Tools/views/Sliderator/SlideratorView.xaml.cs b/Mapping Tools/views/Sliderator/SlideratorView.xaml.cs index 95a4670a..97128762 100644 --- a/Mapping Tools/views/Sliderator/SlideratorView.xaml.cs +++ b/Mapping Tools/views/Sliderator/SlideratorView.xaml.cs @@ -689,8 +689,8 @@ private string Sliderate(SlideratorVm arg, BackgroundWorker worker) { 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)); + timingPointsChanges.Add(new TimingPointsChange(tpOn, mpb:true, unInherited:true, omitFirstBarLine:true, fuzzyness:0)); + timingPointsChanges.Add(new TimingPointsChange(tpAfter, mpb:true, unInherited:true, omitFirstBarLine:true, fuzzyness:0)); clone.Time -= 1; } diff --git a/Mapping Tools/views/TimingCopier/TimingCopierView.xaml.cs b/Mapping Tools/views/TimingCopier/TimingCopierView.xaml.cs index 4f21af5f..12c4668f 100644 --- a/Mapping Tools/views/TimingCopier/TimingCopierView.xaml.cs +++ b/Mapping Tools/views/TimingCopier/TimingCopierView.xaml.cs @@ -92,7 +92,7 @@ private string Copy_Timing(TimingCopierVm arg, BackgroundWorker worker, DoWorkEv // Add redlines List redlines = timingFrom.GetAllRedlines(); foreach (TimingPoint tp in redlines) { - timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true, meter: true, inherited: true, omitFirstBarLine: true)); + timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true, meter: true, unInherited: true, omitFirstBarLine: true)); } // Apply timing changes