From 89c159bf6960da390407ccc18c45a073edf7608b Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Wed, 30 Oct 2024 23:30:07 +0100 Subject: [PATCH 1/8] Implement twin support in beatmap converter --- .../SentakkiBeatmapConverter.HitCircle.cs | 7 +- .../SentakkiBeatmapConverter.Slider.cs | 7 +- .../Converter/SentakkiBeatmapConverter.cs | 100 ++++++++++++++++-- .../Beatmaps/Converter/TwinFlags.cs | 14 +++ .../Beatmaps/Converter/TwinPattern.cs | 80 ++++++++++++++ 5 files changed, 193 insertions(+), 15 deletions(-) create mode 100644 osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinFlags.cs create mode 100644 osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs index a81ebcbcd..15d8833fd 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs @@ -7,15 +7,16 @@ namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter; public partial class SentakkiBeatmapConverter { - private Tap convertHitCircle(HitObject original) + private Tap convertHitCircle(HitObject original) => convertHitCircle(original, currentLane, original.StartTime); + private Tap convertHitCircle(HitObject original, int lane, double startTime) { bool isBreak = original.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH); Tap result = new Tap { - Lane = currentLane.NormalizePath(), + Lane = lane.NormalizePath(), Samples = original.Samples, - StartTime = original.StartTime, + StartTime = startTime, Break = isBreak }; diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs index ea945faff..9f4b34545 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs @@ -14,7 +14,8 @@ namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter; public partial class SentakkiBeatmapConverter { - private SentakkiHitObject convertSlider(HitObject original) + private SentakkiHitObject convertSlider(HitObject original) => convertSlider(original, currentLane); + private SentakkiHitObject convertSlider(HitObject original, int lane) { double duration = ((IHasDuration)original).Duration; @@ -26,7 +27,7 @@ private SentakkiHitObject convertSlider(HitObject original) if (isSuitableSlider) { - var slide = tryConvertToSlide(original, currentLane); + var slide = tryConvertToSlide(original, lane); if (slide is not null) return slide.Value.Item1; @@ -34,7 +35,7 @@ private SentakkiHitObject convertSlider(HitObject original) var hold = new Hold { - Lane = currentLane = currentLane.NormalizePath(), + Lane = lane.NormalizePath(), Break = isBreak, StartTime = original.StartTime, Duration = duration, diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs index 3854778c5..629ddf6d1 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -25,12 +26,16 @@ public partial class SentakkiBeatmapConverter : BeatmapConverter ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) { - SentakkiHitObject result = original switch + SentakkiHitObject result; + switch (original) { - IHasPathWithRepeats => convertSlider(original), - IHasDuration => convertSpinner(original), - _ => convertHitCircle(original) - }; + case IHasPathWithRepeats s: + result = convertSlider(original); + + bool startClap = s.NodeSamples.First().Any(h => h.Name == HitSampleInfo.HIT_CLAP); + bool endClap = s.NodeSamples.Last().Any(h => h.Name == HitSampleInfo.HIT_CLAP); + + if (startClap && !endClap) + { + if (!isChronologicallyClose(lastTwinTime, original.StartTime) && newComboSinceLastTwin) + currentPattern.NewPattern(); + + newComboSinceLastTwin = false; + lastTwinTime = original.GetEndTime(); + + int originalLane = ((SentakkiLanedHitObject)result).Lane; + int twinLane = currentPattern.getNextLane(originalLane).NormalizePath(); + + if (twinLane != originalLane) + yield return convertHitCircle(original, twinLane, original.StartTime); + } + else if (startClap && endClap) + { + if (!isChronologicallyClose(lastTwinTime, original.StartTime) && newComboSinceLastTwin) + currentPattern.NewPattern(); + + newComboSinceLastTwin = false; + lastTwinTime = original.GetEndTime(); + + int originalLane = ((SentakkiLanedHitObject)result).Lane; + int twinLane = currentPattern.getNextLane(originalLane).NormalizePath(); + + if (twinLane != originalLane) + yield return convertSlider(original, twinLane); + } + else if (endClap) + { + if (!isChronologicallyClose(lastTwinTime, original.GetEndTime()) && newComboSinceLastTwin) + currentPattern.NewPattern(); + + newComboSinceLastTwin = false; + lastTwinTime = original.GetEndTime(); + + int originalLane = ((SentakkiLanedHitObject)result).Lane; + int twinLane = currentPattern.getNextLane(originalLane).NormalizePath(); + + if (twinLane != originalLane) + yield return convertHitCircle(original, twinLane, original.GetEndTime()); + } + + break; + case IHasDuration: + result = convertSpinner(original); + break; + default: + result = convertHitCircle(original); + + if (original.Samples.Any(h => h.Name == HitSampleInfo.HIT_CLAP)) + { + if (!isChronologicallyClose(lastTwinTime, original.StartTime) && newComboSinceLastTwin) + currentPattern.NewPattern(); + + newComboSinceLastTwin = false; + lastTwinTime = original.GetEndTime(); + + int originalLane = ((SentakkiLanedHitObject)result).Lane; + int twinLane = currentPattern.getNextLane(originalLane).NormalizePath(); + + if (twinLane != originalLane) + yield return convertHitCircle(original, twinLane, original.StartTime); + } + break; + } // Update the lane to be used by the next hitobject updateCurrentLane(original, result); @@ -73,6 +148,9 @@ private void updateCurrentLane(HitObject original, SentakkiHitObject converted) if (next is null) return; + if (((IHasCombo)next).NewCombo) + newComboSinceLastTwin = true; + // If the next note is far off, we start from a fresh slate if (!isChronologicallyClose(original, next)) { @@ -228,11 +306,15 @@ private static int getClosestLaneFor(float angle) return closestLane; } - private bool isChronologicallyClose(HitObject a, HitObject b) + private bool isChronologicallyClose(double a, double b) { - double timeDelta = b.StartTime - a.GetEndTime(); - double beatLength = beatmap.ControlPointInfo.TimingPointAt(b.StartTime).BeatLength; + double timeDelta = b - a; + double beatLength = beatmap.ControlPointInfo.TimingPointAt(b).BeatLength; return timeDelta <= beatLength; } + private bool isChronologicallyClose(HitObject a, HitObject b) + { + return isChronologicallyClose(a.GetEndTime(), b.StartTime); + } } diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinFlags.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinFlags.cs new file mode 100644 index 000000000..5be440bbf --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinFlags.cs @@ -0,0 +1,14 @@ +using System; + +namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter; + +[Flags] +public enum TwinFlags +{ + None = 0, + Mirror = 1 << 1, // The lane is horizontally mirrored from the main note + Cycle = 1 << 2, // Cycles between 1 or more different lanes, prechosen + SpinCW = 1 << 3, // Increments lane by 1 clockwise + SpinCCW = 1 << 4, // Decrements lane by 1 counterclockwise + Copy = 1 << 5, // Simply copies the main note, but with an offset +} diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs new file mode 100644 index 000000000..038301fdb --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using Markdig.Extensions.Yaml; +using osu.Framework.Extensions.EnumExtensions; + +namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter; + +public class TwinPattern +{ + private TwinFlags flags; + + private List cycleLanes = new List(); + private int cycleIndex = 0; + + private int originLane = 0; + + private int spinIncrement = 0; + + private int copyOffset = 1; + + private Random rng; + + public TwinPattern(Random rng) + { + this.rng = rng; + NewPattern(); + } + + public void NewPattern() + { + flags = (TwinFlags)(1 << (rng.Next(1, 6))); + originLane = rng.Next(0, 4); + + if (flags.HasFlagFast(TwinFlags.Cycle)) + { + cycleLanes.Clear(); + cycleLanes.Add(rng.Next(0, 8)); + cycleIndex = 0; + + float prob = 0.75f; + + while (true) + { + if (rng.NextSingle() > prob) + break; + + cycleLanes.Add(rng.Next(0, 8)); + prob *= 0.5f; + } + } + } + + public int getNextLane(int currentLane) + { + if (flags.HasFlagFast(TwinFlags.Mirror)) + return 7 - currentLane; + + if (flags.HasFlagFast(TwinFlags.SpinCW)) + return originLane + (++spinIncrement); + + if (flags.HasFlagFast(TwinFlags.SpinCCW)) + return originLane + (--spinIncrement); + + if (flags.HasFlagFast(TwinFlags.Cycle)) + { + int tmp = originLane + cycleLanes[cycleIndex]; + cycleIndex = (cycleIndex + 1) % cycleLanes.Count; + + return tmp; + } + + if (flags.HasFlagFast(TwinFlags.Copy)) + { + return currentLane + copyOffset; + } + return currentLane; + } + + +} From edd1ee13fe6967b20b618822fd6a5aee07670e16 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 3 Nov 2024 00:30:43 +0100 Subject: [PATCH 2/8] Add support for converting repeats into taps --- .../SentakkiBeatmapConverter.HitCircle.cs | 2 +- .../SentakkiBeatmapConverter.Slider.cs | 8 +- .../Converter/SentakkiBeatmapConverter.cs | 121 +++++++++--------- .../Beatmaps/Converter/TwinPattern.cs | 4 + .../Mods/SentakkiModExperimental.cs | 6 +- 5 files changed, 70 insertions(+), 71 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs index 15d8833fd..b06e9b1a7 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs @@ -14,7 +14,7 @@ private Tap convertHitCircle(HitObject original, int lane, double startTime) Tap result = new Tap { - Lane = lane.NormalizePath(), + Lane = lane, Samples = original.Samples, StartTime = startTime, Break = isBreak diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs index 9f4b34545..67382d814 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs @@ -14,8 +14,8 @@ namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter; public partial class SentakkiBeatmapConverter { - private SentakkiHitObject convertSlider(HitObject original) => convertSlider(original, currentLane); - private SentakkiHitObject convertSlider(HitObject original, int lane) + private SentakkiHitObject convertSlider(HitObject original) => convertSlider(original, currentLane, false); + private SentakkiHitObject convertSlider(HitObject original, int lane, bool forceHoldNote) { double duration = ((IHasDuration)original).Duration; @@ -25,7 +25,7 @@ private SentakkiHitObject convertSlider(HitObject original, int lane) bool isBreak = slider.NodeSamples[0].Any(s => s.Name == HitSampleInfo.HIT_FINISH); - if (isSuitableSlider) + if (isSuitableSlider && !forceHoldNote) { var slide = tryConvertToSlide(original, lane); @@ -35,7 +35,7 @@ private SentakkiHitObject convertSlider(HitObject original, int lane) var hold = new Hold { - Lane = lane.NormalizePath(), + Lane = lane, Break = isBreak, StartTime = original.StartTime, Duration = duration, diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs index 629ddf6d1..46b7336d9 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -56,6 +57,18 @@ public SentakkiBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) : base(beatma currentLane = getClosestLaneFor(angle); } + private bool tryGetLaneForTwinNote(double targetTime, out int twinLane) + { + if (!isChronologicallyClose(lastTwinTime, targetTime) && newComboSinceLastTwin) + currentPattern.NewPattern(); + + newComboSinceLastTwin = false; + lastTwinTime = targetTime; + + twinLane = currentPattern.getNextLane(currentLane).NormalizePath(); + return currentLane != twinLane; + } + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) { SentakkiHitObject result; @@ -63,77 +76,61 @@ protected override IEnumerable ConvertHitObject(HitObject ori { case IHasPathWithRepeats s: result = convertSlider(original); - - bool startClap = s.NodeSamples.First().Any(h => h.Name == HitSampleInfo.HIT_CLAP); - bool endClap = s.NodeSamples.Last().Any(h => h.Name == HitSampleInfo.HIT_CLAP); - - if (startClap && !endClap) - { - if (!isChronologicallyClose(lastTwinTime, original.StartTime) && newComboSinceLastTwin) - currentPattern.NewPattern(); - - newComboSinceLastTwin = false; - lastTwinTime = original.GetEndTime(); - - int originalLane = ((SentakkiLanedHitObject)result).Lane; - int twinLane = currentPattern.getNextLane(originalLane).NormalizePath(); - - if (twinLane != originalLane) - yield return convertHitCircle(original, twinLane, original.StartTime); - } - else if (startClap && endClap) - { - if (!isChronologicallyClose(lastTwinTime, original.StartTime) && newComboSinceLastTwin) - currentPattern.NewPattern(); - - newComboSinceLastTwin = false; - lastTwinTime = original.GetEndTime(); - - int originalLane = ((SentakkiLanedHitObject)result).Lane; - int twinLane = currentPattern.getNextLane(originalLane).NormalizePath(); - - if (twinLane != originalLane) - yield return convertSlider(original, twinLane); - } - else if (endClap) - { - if (!isChronologicallyClose(lastTwinTime, original.GetEndTime()) && newComboSinceLastTwin) - currentPattern.NewPattern(); - - newComboSinceLastTwin = false; - lastTwinTime = original.GetEndTime(); - - int originalLane = ((SentakkiLanedHitObject)result).Lane; - int twinLane = currentPattern.getNextLane(originalLane).NormalizePath(); - - if (twinLane != originalLane) - yield return convertHitCircle(original, twinLane, original.GetEndTime()); - } - break; case IHasDuration: result = convertSpinner(original); break; default: result = convertHitCircle(original); - - if (original.Samples.Any(h => h.Name == HitSampleInfo.HIT_CLAP)) - { - if (!isChronologicallyClose(lastTwinTime, original.StartTime) && newComboSinceLastTwin) - currentPattern.NewPattern(); - - newComboSinceLastTwin = false; - lastTwinTime = original.GetEndTime(); - - int originalLane = ((SentakkiLanedHitObject)result).Lane; - int twinLane = currentPattern.getNextLane(originalLane).NormalizePath(); - - if (twinLane != originalLane) - yield return convertHitCircle(original, twinLane, original.StartTime); - } break; } + // Twin note generation section + if (ConversionFlags.HasFlagFast(ConversionFlags.twinNotes)) + { + switch (original) + { + case IHasPathWithRepeats s: + bool allClaps = s.NodeSamples.All(ns => ns.Any(h => h.Name == HitSampleInfo.HIT_CLAP)); + + if (allClaps) + { + if (tryGetLaneForTwinNote(original.StartTime, out int twinLane)) + yield return convertSlider(original, twinLane, false); + break; + } + + // Fallback to using taps for each node with a clap + double spansDuration = s.Duration / (s.RepeatCount + 1); + + for (int i = 0; i < s.NodeSamples.Count; ++i) + { + var samples = s.NodeSamples[i]; + if (samples.All(h => h.Name != HitSampleInfo.HIT_CLAP)) + continue; + + double targetTime = original.StartTime + spansDuration * i; + bool isBreak = samples.Any(h => h.Name == HitSampleInfo.HIT_CLAP); + + if (tryGetLaneForTwinNote(original.StartTime, out int twinLane)) + { + var sho = (SentakkiLanedHitObject)convertHitCircle(original, twinLane, targetTime); + sho.Break = isBreak; + sho.Samples = samples; + }; + } + + break; + default: + if (original.Samples.Any(h => h.Name == HitSampleInfo.HIT_CLAP)) + { + if (tryGetLaneForTwinNote(original.StartTime, out int twinLane)) + yield return convertHitCircle(original, twinLane, original.StartTime); + } + break; + } + } + // Update the lane to be used by the next hitobject updateCurrentLane(original, result); diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs index 038301fdb..1a609a972 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; using Markdig.Extensions.Yaml; using osu.Framework.Extensions.EnumExtensions; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Beatmaps; namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter; @@ -23,6 +26,7 @@ public class TwinPattern public TwinPattern(Random rng) { this.rng = rng; + NewPattern(); } diff --git a/osu.Game.Rulesets.Sentakki/Mods/SentakkiModExperimental.cs b/osu.Game.Rulesets.Sentakki/Mods/SentakkiModExperimental.cs index b46fa848d..acaecbb74 100644 --- a/osu.Game.Rulesets.Sentakki/Mods/SentakkiModExperimental.cs +++ b/osu.Game.Rulesets.Sentakki/Mods/SentakkiModExperimental.cs @@ -42,10 +42,8 @@ public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter) if (EnableSlideFans.Value) sentakkiBeatmapConverter.flags |= ConversionFlags.fanSlides; - if (!OldConversion.Value) - return; - - sentakkiBeatmapConverter.flags |= ConversionFlags.oldConverter; + if (OldConversion.Value) + sentakkiBeatmapConverter.flags |= ConversionFlags.oldConverter; if (EnableTwinNotes.Value) sentakkiBeatmapConverter.flags |= ConversionFlags.twinNotes; From 320aa7a73665ea598987fe72c9c2b8a42121b1ef Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 3 Nov 2024 01:39:24 +0100 Subject: [PATCH 3/8] Fix twins being generated with fans --- .../Converter/SentakkiBeatmapConverter.Slider.cs | 14 +++++++------- .../Converter/SentakkiBeatmapConverter.cs | 16 +++++++++++++--- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs index 67382d814..5d6c02604 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs @@ -14,8 +14,8 @@ namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter; public partial class SentakkiBeatmapConverter { - private SentakkiHitObject convertSlider(HitObject original) => convertSlider(original, currentLane, false); - private SentakkiHitObject convertSlider(HitObject original, int lane, bool forceHoldNote) + private SentakkiHitObject convertSlider(HitObject original) => convertSlider(original, currentLane, false, true); + private SentakkiHitObject convertSlider(HitObject original, int lane, bool forceHoldNote, bool allowFans) { double duration = ((IHasDuration)original).Duration; @@ -27,7 +27,7 @@ private SentakkiHitObject convertSlider(HitObject original, int lane, bool force if (isSuitableSlider && !forceHoldNote) { - var slide = tryConvertToSlide(original, lane); + var slide = tryConvertToSlide(original, lane, allowFans); if (slide is not null) return slide.Value.Item1; @@ -44,11 +44,11 @@ private SentakkiHitObject convertSlider(HitObject original, int lane, bool force return hold; } - private (Slide, int endLane)? tryConvertToSlide(HitObject original, int lane) + private (Slide, int endLane)? tryConvertToSlide(HitObject original, int lane, bool allowFans) { var nodeSamples = ((IHasPathWithRepeats)original).NodeSamples; - var selectedPath = chooseSlidePartFor(original); + var selectedPath = chooseSlidePartFor(original, allowFans); if (selectedPath is null) return null; @@ -81,14 +81,14 @@ private SentakkiHitObject convertSlider(HitObject original, int lane, bool force return (slide, end); } - private SlideBodyPart[]? chooseSlidePartFor(HitObject original) + private SlideBodyPart[]? chooseSlidePartFor(HitObject original, bool allowFans) { double velocity = original is IHasSliderVelocity slider ? (slider.SliderVelocityMultiplier * beatmap.Difficulty.SliderMultiplier) : 1; double duration = ((IHasDuration)original).Duration; double adjustedDuration = duration * velocity; var candidates = SlidePaths.VALIDPATHS.AsEnumerable(); - if (!ConversionFlags.HasFlag(ConversionFlags.fanSlides)) + if (!ConversionFlags.HasFlag(ConversionFlags.fanSlides) || !allowFans) candidates = candidates.Where(p => p.SlidePart.Shape != SlidePaths.PathShapes.Fan); if (!ConversionFlags.HasFlag(ConversionFlags.disableCompositeSlides)) diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs index 46b7336d9..8cfe439ee 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs @@ -93,10 +93,18 @@ protected override IEnumerable ConvertHitObject(HitObject ori case IHasPathWithRepeats s: bool allClaps = s.NodeSamples.All(ns => ns.Any(h => h.Name == HitSampleInfo.HIT_CLAP)); - if (allClaps) + double fanStartTime = double.MaxValue; + if (result is Slide slide) + { + var slidePath = slide.SlideInfoList[0].SlidePath; + if (slidePath.EndsWithSlideFan) + fanStartTime = slide.StartTime + slide.Duration * slidePath.FanStartProgress; + } + + if (allClaps && fanStartTime != double.MaxValue) { if (tryGetLaneForTwinNote(original.StartTime, out int twinLane)) - yield return convertSlider(original, twinLane, false); + yield return convertSlider(original, twinLane, false, false); break; } @@ -110,7 +118,9 @@ protected override IEnumerable ConvertHitObject(HitObject ori continue; double targetTime = original.StartTime + spansDuration * i; - bool isBreak = samples.Any(h => h.Name == HitSampleInfo.HIT_CLAP); + + if (targetTime >= fanStartTime) + break; if (tryGetLaneForTwinNote(original.StartTime, out int twinLane)) { From 842a858a98a03868cee73ec1fe4b7cdeb2484c0c Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 3 Nov 2024 01:40:22 +0100 Subject: [PATCH 4/8] Use whistle sample for EX state --- .../Converter/SentakkiBeatmapConverter.HitCircle.cs | 4 +++- .../Converter/SentakkiBeatmapConverter.Slider.cs | 10 +++++++--- .../Beatmaps/Converter/SentakkiBeatmapConverter.cs | 8 +++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs index b06e9b1a7..d255e5cd8 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs @@ -11,13 +11,15 @@ public partial class SentakkiBeatmapConverter private Tap convertHitCircle(HitObject original, int lane, double startTime) { bool isBreak = original.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH); + bool isSoft = original.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE); Tap result = new Tap { Lane = lane, Samples = original.Samples, StartTime = startTime, - Break = isBreak + Break = isBreak, + Ex = isSoft }; return result; diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs index 5d6c02604..0a9ca0e04 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs @@ -23,8 +23,6 @@ private SentakkiHitObject convertSlider(HitObject original, int lane, bool force bool isSuitableSlider = !isLazySlider(original); - bool isBreak = slider.NodeSamples[0].Any(s => s.Name == HitSampleInfo.HIT_FINISH); - if (isSuitableSlider && !forceHoldNote) { var slide = tryConvertToSlide(original, lane, allowFans); @@ -33,6 +31,9 @@ private SentakkiHitObject convertSlider(HitObject original, int lane, bool force return slide.Value.Item1; } + bool isBreak = slider.NodeSamples[0].Any(s => s.Name == HitSampleInfo.HIT_FINISH); + bool isSoft = slider.NodeSamples[0].Any(s => s.Name == HitSampleInfo.HIT_WHISTLE); + var hold = new Hold { Lane = lane, @@ -40,6 +41,7 @@ private SentakkiHitObject convertSlider(HitObject original, int lane, bool force StartTime = original.StartTime, Duration = duration, NodeSamples = slider.NodeSamples, + Ex = isSoft, }; return hold; } @@ -55,6 +57,7 @@ private SentakkiHitObject convertSlider(HitObject original, int lane, bool force bool tailBreak = nodeSamples.Last().Any(s => s.Name == HitSampleInfo.HIT_FINISH); bool headBreak = nodeSamples.First().Any(s => s.Name == HitSampleInfo.HIT_FINISH); + bool isSoft = original.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE); int endOffset = selectedPath.Sum(p => p.EndOffset); @@ -75,7 +78,8 @@ private SentakkiHitObject convertSlider(HitObject original, int lane, bool force Lane = lane.NormalizePath(), StartTime = original.StartTime, Samples = nodeSamples.FirstOrDefault(), - Break = headBreak + Break = headBreak, + Ex = isSoft }; return (slide, end); diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs index 8cfe439ee..79c5afed1 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs @@ -122,11 +122,17 @@ protected override IEnumerable ConvertHitObject(HitObject ori if (targetTime >= fanStartTime) break; - if (tryGetLaneForTwinNote(original.StartTime, out int twinLane)) + bool isBreak = samples.Any(h => h.Name == HitSampleInfo.HIT_FINISH); + bool isSoft = samples.Any(h => h.Name == HitSampleInfo.HIT_WHISTLE); + + if (tryGetLaneForTwinNote(targetTime, out int twinLane)) { var sho = (SentakkiLanedHitObject)convertHitCircle(original, twinLane, targetTime); sho.Break = isBreak; sho.Samples = samples; + sho.Ex = isSoft; + + yield return sho; }; } From 940a63e0b80c5a2b6a025e5f8f2763286fcb1ec8 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Thu, 7 Nov 2024 18:01:18 +0100 Subject: [PATCH 5/8] Fix minor twin generation bug --- .../Beatmaps/Converter/SentakkiBeatmapConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs index 79c5afed1..9506e3784 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs @@ -101,7 +101,7 @@ protected override IEnumerable ConvertHitObject(HitObject ori fanStartTime = slide.StartTime + slide.Duration * slidePath.FanStartProgress; } - if (allClaps && fanStartTime != double.MaxValue) + if (allClaps && fanStartTime == double.MaxValue) { if (tryGetLaneForTwinNote(original.StartTime, out int twinLane)) yield return convertSlider(original, twinLane, false, false); From 75d764def6e51423cfb6c04115d038af69f1bb00 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Thu, 7 Nov 2024 18:01:30 +0100 Subject: [PATCH 6/8] Increase twin pattern variety --- .../Beatmaps/Converter/TwinPattern.cs | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs index 1a609a972..42cf36225 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs @@ -10,6 +10,16 @@ namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter; public class TwinPattern { + private static readonly TwinFlags[] allowedFlags = new TwinFlags[]{ + TwinFlags.None, + TwinFlags.SpinCW, + TwinFlags.SpinCCW, + TwinFlags.Cycle, + TwinFlags.Copy, + TwinFlags.Mirror, + TwinFlags.Copy | TwinFlags.Mirror + }; + private TwinFlags flags; private List cycleLanes = new List(); @@ -32,8 +42,8 @@ public TwinPattern(Random rng) public void NewPattern() { - flags = (TwinFlags)(1 << (rng.Next(1, 6))); - originLane = rng.Next(0, 4); + flags = allowedFlags[rng.Next(0, allowedFlags.Length)]; + originLane = rng.Next(0, 8); if (flags.HasFlagFast(TwinFlags.Cycle)) { @@ -52,19 +62,14 @@ public void NewPattern() prob *= 0.5f; } } + else if (flags.HasFlagFast(TwinFlags.Copy)) + { + copyOffset = rng.Next(1, 7); + } } public int getNextLane(int currentLane) { - if (flags.HasFlagFast(TwinFlags.Mirror)) - return 7 - currentLane; - - if (flags.HasFlagFast(TwinFlags.SpinCW)) - return originLane + (++spinIncrement); - - if (flags.HasFlagFast(TwinFlags.SpinCCW)) - return originLane + (--spinIncrement); - if (flags.HasFlagFast(TwinFlags.Cycle)) { int tmp = originLane + cycleLanes[cycleIndex]; @@ -73,11 +78,21 @@ public int getNextLane(int currentLane) return tmp; } + if (flags.HasFlagFast(TwinFlags.SpinCW)) + return originLane + (++spinIncrement); + + if (flags.HasFlagFast(TwinFlags.SpinCCW)) + return originLane + (--spinIncrement); + + + int result = currentLane; if (flags.HasFlagFast(TwinFlags.Copy)) - { - return currentLane + copyOffset; - } - return currentLane; + result += copyOffset; + + if (flags.HasFlagFast(TwinFlags.Mirror)) + result = 7 - result; + + return result.NormalizePath(); } From 5df610ae063e5e289a8854522b471d83e2e39b96 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Fri, 15 Nov 2024 10:55:33 +0100 Subject: [PATCH 7/8] Respect twin slides experimental option --- .../Beatmaps/Converter/SentakkiBeatmapConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs index 9506e3784..ec06becba 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs @@ -104,7 +104,7 @@ protected override IEnumerable ConvertHitObject(HitObject ori if (allClaps && fanStartTime == double.MaxValue) { if (tryGetLaneForTwinNote(original.StartTime, out int twinLane)) - yield return convertSlider(original, twinLane, false, false); + yield return convertSlider(original, twinLane, !ConversionFlags.HasFlagFast(ConversionFlags.twinSlides), false); break; } From fe68e8657a96c0fc39c690981fc735de59bd52bf Mon Sep 17 00:00:00 2001 From: codefactor-io Date: Fri, 15 Nov 2024 09:55:56 +0000 Subject: [PATCH 8/8] [CodeFactor] Apply fixes --- .../Beatmaps/Converter/SentakkiBeatmapConverter.cs | 8 ++++---- .../Beatmaps/Converter/TwinPattern.cs | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs index ec06becba..2c5525aac 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs @@ -42,7 +42,7 @@ public SentakkiBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) : base(beatma this.beatmap = beatmap; // Taking this from osu specific information that we need - circleRadius = 54.4f - 4.48f * beatmap.Difficulty.CircleSize; + circleRadius = 54.4f - (4.48f * beatmap.Difficulty.CircleSize); // Prep an RNG with a seed generated from beatmap diff var difficulty = beatmap.BeatmapInfo.Difficulty; @@ -98,7 +98,7 @@ protected override IEnumerable ConvertHitObject(HitObject ori { var slidePath = slide.SlideInfoList[0].SlidePath; if (slidePath.EndsWithSlideFan) - fanStartTime = slide.StartTime + slide.Duration * slidePath.FanStartProgress; + fanStartTime = slide.StartTime + (slide.Duration * slidePath.FanStartProgress); } if (allClaps && fanStartTime == double.MaxValue) @@ -117,7 +117,7 @@ protected override IEnumerable ConvertHitObject(HitObject ori if (samples.All(h => h.Name != HitSampleInfo.HIT_CLAP)) continue; - double targetTime = original.StartTime + spansDuration * i; + double targetTime = original.StartTime + (spansDuration * i); if (targetTime >= fanStartTime) break; @@ -133,7 +133,7 @@ protected override IEnumerable ConvertHitObject(HitObject ori sho.Ex = isSoft; yield return sho; - }; + } } break; diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs index 42cf36225..2988e3852 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs @@ -94,6 +94,4 @@ public int getNextLane(int currentLane) return result.NormalizePath(); } - - }