Skip to content

Commit

Permalink
Merge pull request #481 from LumpBloom7/RevampedBeatmapConverter
Browse files Browse the repository at this point in the history
Implement revamped beatmap converter
  • Loading branch information
LumpBloom7 authored Sep 24, 2023
2 parents 7bea60d + f9fa18f commit 085a983
Show file tree
Hide file tree
Showing 20 changed files with 736 additions and 112 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"dotnet.defaultSolution": "osu.Game.Rulesets.Sentakki.sln"
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public TestSceneAllSlides()
});
}

protected SentakkiSlidePath CreatePattern() => SlidePaths.CreateSlidePath(SlidePaths.VALIDPATHS[id].parameters);
protected SentakkiSlidePath CreatePattern() => SlidePaths.CreateSlidePath(SlidePaths.VALIDPATHS[id].SlidePart);

protected override void LoadComplete()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public TestSceneTouchNote()

private void testAllPositions(bool auto = false)
{
foreach (var position in SentakkiBeatmapConverter.VALID_TOUCH_POSITIONS)
foreach (var position in SentakkiBeatmapConverterOld.VALID_TOUCH_POSITIONS)
{
var circle = new Touch
{
Expand Down
34 changes: 34 additions & 0 deletions osu.Game.Rulesets.Sentakki/Beatmaps/CompositeBeatmapConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Linq;
using System.Threading;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Sentakki.Beatmaps.Converter;
using osu.Game.Rulesets.Sentakki.Objects;

namespace osu.Game.Rulesets.Sentakki.Beatmaps;

public class CompositeBeatmapConverter : BeatmapConverter<SentakkiHitObject>
{
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasPosition);

public ConversionFlags flags;

private Ruleset ruleset;

public CompositeBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) : base(beatmap, ruleset)
{
this.ruleset = ruleset;
}

protected override Beatmap<SentakkiHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
BeatmapConverter<SentakkiHitObject> converter;

if (flags.HasFlag(ConversionFlags.oldConverter))
converter = new SentakkiBeatmapConverterOld(original, ruleset) { ConversionFlags = flags };
else
converter = new SentakkiBeatmapConverter(original, ruleset) { ConversionFlags = flags };

return ((SentakkiBeatmap)converter.Convert(cancellationToken)) ?? base.ConvertBeatmap(original, cancellationToken);
}
}
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 ConversionFlags
{
none = 0,
twinNotes = 1,
twinSlides = 2,
fanSlides = 4,
oldConverter = 8,
disableCompositeSlides = 16
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Linq;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Sentakki.Objects;

namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter;

public partial class SentakkiBeatmapConverter
{
private Tap convertHitCircle(HitObject original)
{
bool isBreak = original.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH);

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

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Sentakki.Extensions;
using osu.Game.Rulesets.Sentakki.Objects;

namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter;

public partial class SentakkiBeatmapConverter
{
private SentakkiHitObject convertSlider(HitObject original)
{
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)
{
var slide = tryConvertToSlide(original, currentLane);

if (slide is not null)
return slide.Value.Item1;
}

var hold = new Hold
{
Lane = currentLane = currentLane.NormalizePath(),
Break = isBreak,
StartTime = original.StartTime,
Duration = duration,
NodeSamples = slider.NodeSamples,
};
return hold;
}

private (Slide, int endLane)? tryConvertToSlide(HitObject original, int lane)
{
var nodeSamples = ((IHasPathWithRepeats)original).NodeSamples;

var selectedPath = chooseSlidePartFor(original);

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

int endOffset = selectedPath.Sum(p => p.EndOffset);

int end = lane + endOffset;

var slide = new Slide
{
SlideInfoList = new List<SlideBodyInfo>()
{
new SlideBodyInfo
{
SlidePathParts = selectedPath,
Duration = ((IHasDuration)original).Duration,
Break = tailBreak,
ShootDelay = 0.5f,
}
},
Lane = lane.NormalizePath(),
StartTime = original.StartTime,
NodeSamples = nodeSamples,
Break = headBreak
};

return (slide, end);
}

private SlideBodyPart[]? chooseSlidePartFor(HitObject original)
{
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))
candidates = candidates.Where(p => p.SlidePart.Shape != SlidePaths.PathShapes.Fan);

if (!ConversionFlags.HasFlag(ConversionFlags.disableCompositeSlides))
{
List<SlideBodyPart> parts = new List<SlideBodyPart>();

double durationLeft = duration;

SlideBodyPart? lastPart = null;

double velocityAdjustmentFactor = 1 + (0.5 / velocity);

while (true)
{
var nextChoices = candidates.Where(p => p.MinDuration * velocityAdjustmentFactor < durationLeft)
.Shuffle(rng)
.SkipWhile(p => p.SlidePart.Shape == SlidePaths.PathShapes.Circle && !isValidCircleComposition(p.SlidePart, lastPart));

if (!nextChoices.Any())
break;

var chosen = nextChoices.First();

durationLeft -= chosen.MinDuration * velocityAdjustmentFactor;
parts.Add((lastPart = chosen.SlidePart).Value);
}

if (!parts.Any())
return null;

return parts.ToArray();
}
else
{
// Find the part that is the closest
return new[]
{
candidates.GroupBy(t => getDelta(t.MinDuration))
.OrderBy(g => g.Key)
.Take(5)
.ProbabilityPick(t => t.Key, rng)
.Shuffle(rng)
.First()
.SlidePart
};
}

double getDelta(double d)
{
double diff = adjustedDuration - (d * 2);
if (diff > 0) diff *= 3; // We don't want to overly favor longer slides when a shorter one is available

return Math.Round(Math.Abs(diff) * 0.02) * 100; // Round to nearest 100ms
}
}

private static bool isValidCircleComposition(SlideBodyPart part, SlideBodyPart? previousPart)
{
if (previousPart is null)
return true;

if (previousPart.Value.Shape != SlidePaths.PathShapes.Circle)
return true;

if (part.Mirrored != previousPart.Value.Mirrored)
return true;

return previousPart.Value.EndOffset == 0;
}

// This checks whether a slider can be completed without moving the mouse at all.
private bool isLazySlider(HitObject hitObject)
{
const float follow_radius_scale = 2.4f;

if (hitObject is not IHasPathWithRepeats slider)
return false;

float distanceCutoffSquared = MathF.Pow(circleRadius * follow_radius_scale, 2);

double spanDuration = slider.Duration / (slider.RepeatCount + 1);
var difficulty = beatmap.BeatmapInfo.Difficulty;

var controlPointInfo = (LegacyControlPointInfo)beatmap.ControlPointInfo;

TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(hitObject.StartTime);

double sliderVelocity = (hitObject is IHasSliderVelocity sv) ? sv.SliderVelocityMultiplier : DifficultyControlPoint.DEFAULT.SliderVelocity;
double scoringDistance = 100 * difficulty.SliderMultiplier * sliderVelocity;
double velocity = scoringDistance / timingPoint.BeatLength;
double tickDistance = scoringDistance / difficulty.SliderTickRate;
double legacyLastTickOffset = (hitObject as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0;

var sliderEvents = SliderEventGenerator.Generate(hitObject.StartTime, spanDuration, velocity, tickDistance, slider.Path.Distance, slider.RepeatCount + 1, legacyLastTickOffset,
CancellationToken.None);

var sliderOrigin = slider.Path.PositionAt(0);

// Check if any events such as ticks or repeats are a certain distance from the origin, requiring a cursor move.
return sliderEvents.All(e => !((slider.Path.PositionAt(e.PathProgress) - sliderOrigin).LengthSquared >= distanceCutoffSquared));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Sentakki.Objects;

namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter;

public partial class SentakkiBeatmapConverter
{
private SentakkiHitObject convertSpinner(HitObject original)
{
return new TouchHold
{
StartTime = original.StartTime,
Samples = original.Samples,
Duration = ((IHasDuration)original).Duration
};
}
}
Loading

0 comments on commit 085a983

Please sign in to comment.