Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update new converter to support twins #642

Merged
merged 9 commits into from
Nov 15, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ 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);
bool isSoft = original.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE);

Tap result = new Tap
{
Lane = currentLane.NormalizePath(),
Lane = lane,
Samples = original.Samples,
StartTime = original.StartTime,
Break = isBreak
StartTime = startTime,
Break = isBreak,
Ex = isSoft
};

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,46 +14,50 @@ namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter;

public partial class SentakkiBeatmapConverter
{
private SentakkiHitObject convertSlider(HitObject original)
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;

var slider = (IHasPathWithRepeats)original;

bool isSuitableSlider = !isLazySlider(original);

bool isBreak = slider.NodeSamples[0].Any(s => s.Name == HitSampleInfo.HIT_FINISH);

if (isSuitableSlider)
if (isSuitableSlider && !forceHoldNote)
{
var slide = tryConvertToSlide(original, currentLane);
var slide = tryConvertToSlide(original, lane, allowFans);

if (slide is not null)
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 = currentLane = currentLane.NormalizePath(),
Lane = lane,
Break = isBreak,
StartTime = original.StartTime,
Duration = duration,
NodeSamples = slider.NodeSamples,
Ex = isSoft,
};
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;

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

Expand All @@ -74,20 +78,21 @@ private SentakkiHitObject convertSlider(HitObject original)
Lane = lane.NormalizePath(),
StartTime = original.StartTime,
Samples = nodeSamples.FirstOrDefault(),
Break = headBreak
Break = headBreak,
Ex = isSoft
};

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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
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;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
Expand All @@ -25,18 +27,22 @@
public ConversionFlags ConversionFlags;

private readonly IBeatmap beatmap;

// Current converter state
private StreamDirection activeStreamDirection;
private int currentLane;
private readonly Random rng;

private TwinPattern currentPattern = null!;

private double lastTwinTime = 0;
private bool newComboSinceLastTwin = true;

public SentakkiBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) : base(beatmap, ruleset)
{
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;
Expand All @@ -46,25 +52,107 @@
if (beatmap.HitObjects.Count == 0)
return;

currentPattern = new TwinPattern(rng);
float angle = standard_playfield_center.GetDegreesFromPosition(beatmap.HitObjects[0].GetPosition());
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<SentakkiHitObject> 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);
break;
case IHasDuration:
result = convertSpinner(original);
break;
default:
result = convertHitCircle(original);
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));

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, !ConversionFlags.HasFlagFast(ConversionFlags.twinSlides), 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);

if (targetTime >= fanStartTime)
break;

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

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

yield return result;
}

Check notice on line 155 in osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs

View check run for this annotation

codefactor.io / CodeFactor

osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs#L72-L155

Complex Method
private void updateCurrentLane(HitObject original, SentakkiHitObject converted)
{
HitObject? next = beatmap.HitObjects.GetNext(original);
Expand All @@ -73,6 +161,9 @@
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))
{
Expand Down Expand Up @@ -228,11 +319,15 @@
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);
}
}
14 changes: 14 additions & 0 deletions osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinFlags.cs
Original file line number Diff line number Diff line change
@@ -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
}
97 changes: 97 additions & 0 deletions osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System;
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;

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<int> cycleLanes = new List<int>();
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 = allowedFlags[rng.Next(0, allowedFlags.Length)];
originLane = rng.Next(0, 8);

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;
}
}
else if (flags.HasFlagFast(TwinFlags.Copy))
{
copyOffset = rng.Next(1, 7);
}
}

public int getNextLane(int currentLane)
{
if (flags.HasFlagFast(TwinFlags.Cycle))
{
int tmp = originLane + cycleLanes[cycleIndex];
cycleIndex = (cycleIndex + 1) % cycleLanes.Count;

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))
result += copyOffset;

if (flags.HasFlagFast(TwinFlags.Mirror))
result = 7 - result;

return result.NormalizePath();
}
}
Loading
Loading