Skip to content

Commit

Permalink
Merge pull request #642 from LumpBloom7/Converter-update
Browse files Browse the repository at this point in the history
Update new converter to support twins
  • Loading branch information
LumpBloom7 authored Nov 15, 2024
2 parents a2e8317 + fe68e86 commit 06b7e2d
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 29 deletions.
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 partial class SentakkiBeatmapConverter : BeatmapConverter<SentakkiHitObje
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,18 +52,100 @@ public SentakkiBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) : base(beatma
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);
Expand All @@ -73,6 +161,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))
{
Expand Down Expand Up @@ -228,11 +319,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);
}
}
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

0 comments on commit 06b7e2d

Please sign in to comment.