mirror of
https://github.com/ppy/osu.git
synced 2026-05-17 11:22:54 +08:00
1e8b9a7639
- Closes https://github.com/ppy/osu/issues/30293 - Fixes https://osu.ppy.sh/community/forums/topics/2179339?n=1 Aside from fixing the off-by-one error that I mentioned in https://github.com/ppy/osu/issues/30293#issuecomment-2413801663, this also: - Brings back the behaviour wherein if timing points are arranged very weird and nightcore would play e.g. two first beats in a timing point back-to-back, the second timing point is silent. - Brings back the behaviour wherein the finish sample only plays if `OmitFirstBarLine` on the timing point is disabled. However: - This does not bring back the behaviour wherein hat samples only play if the slider tick rate is even because that only kind of makes sense in common time, and if common time is mixed with waltz time or other time signatures, it just gets weird. - Also stable has zero attempt for compensating for waltz time anyway, lazer's behaviour is bespoke, so that is not going to match any way you cut it. My testing procedure essentially consisted of getting stable to log when it was playing nightcore samples and cross-checking the first 30sec or so of https://osu.ppy.sh/beatmapsets/534385#osu/1131956 (check out the timing of that beatmap, for something ranked it is DEEPLY messed up). I guess I can add test cases if deemed required but I already wasted much more time than I would have liked here...
191 lines
7.4 KiB
C#
191 lines
7.4 KiB
C#
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Audio;
|
|
using osu.Framework.Audio.Track;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Sprites;
|
|
using osu.Framework.Localisation;
|
|
using osu.Game.Audio;
|
|
using osu.Game.Beatmaps.ControlPoints;
|
|
using osu.Game.Configuration;
|
|
using osu.Game.Graphics;
|
|
using osu.Game.Graphics.Containers;
|
|
using osu.Game.Overlays.Settings;
|
|
using osu.Game.Rulesets.Objects;
|
|
using osu.Game.Rulesets.UI;
|
|
using osu.Game.Skinning;
|
|
|
|
namespace osu.Game.Rulesets.Mods
|
|
{
|
|
public abstract class ModNightcore : ModRateAdjust
|
|
{
|
|
public override string Name => "Nightcore";
|
|
public override string Acronym => "NC";
|
|
public override IconUsage? Icon => OsuIcon.ModNightcore;
|
|
public override ModType Type => ModType.DifficultyIncrease;
|
|
public override LocalisableString Description => "Uguuuuuuuu...";
|
|
public override bool Ranked => UsesDefaultConfiguration;
|
|
|
|
[SettingSource("Speed increase", "The actual increase to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
|
|
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(1.5)
|
|
{
|
|
MinValue = 1.01,
|
|
MaxValue = 2,
|
|
Precision = 0.01,
|
|
};
|
|
|
|
private readonly BindableNumber<double> tempoAdjust = new BindableDouble(1);
|
|
private readonly BindableNumber<double> freqAdjust = new BindableDouble(1);
|
|
|
|
private readonly RateAdjustModHelper rateAdjustHelper;
|
|
|
|
protected ModNightcore()
|
|
{
|
|
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);
|
|
|
|
// intentionally not deferring the speed change handling to `RateAdjustModHelper`
|
|
// as the expected result of operation is not the same (nightcore should preserve constant pitch).
|
|
SpeedChange.BindValueChanged(val =>
|
|
{
|
|
freqAdjust.Value = SpeedChange.Default;
|
|
tempoAdjust.Value = val.NewValue / SpeedChange.Default;
|
|
}, true);
|
|
}
|
|
|
|
public override void ApplyToTrack(IAdjustableAudioComponent track)
|
|
{
|
|
track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust);
|
|
track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust);
|
|
}
|
|
|
|
public override double ScoreMultiplier => rateAdjustHelper.ScoreMultiplier;
|
|
}
|
|
|
|
public abstract partial class ModNightcore<TObject> : ModNightcore, IApplicableToDrawableRuleset<TObject>
|
|
where TObject : HitObject
|
|
{
|
|
public void ApplyToDrawableRuleset(DrawableRuleset<TObject> drawableRuleset)
|
|
{
|
|
drawableRuleset.Overlays.Add(new NightcoreBeatContainer());
|
|
}
|
|
|
|
public partial class NightcoreBeatContainer : BeatSyncedContainer
|
|
{
|
|
private PausableSkinnableSound? hatSample;
|
|
private PausableSkinnableSound? clapSample;
|
|
private PausableSkinnableSound? kickSample;
|
|
private PausableSkinnableSound? finishSample;
|
|
|
|
private int? firstBeat;
|
|
private int lastBeat = -1;
|
|
|
|
public NightcoreBeatContainer()
|
|
{
|
|
Divisor = 2;
|
|
}
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load()
|
|
{
|
|
InternalChildren = new Drawable[]
|
|
{
|
|
hatSample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-hat")),
|
|
clapSample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-clap")),
|
|
kickSample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-kick")),
|
|
finishSample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-finish")),
|
|
};
|
|
}
|
|
|
|
private const int bars_per_segment = 4;
|
|
|
|
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
|
|
{
|
|
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
|
|
|
|
int beatsPerBar = timingPoint.TimeSignature.Numerator;
|
|
int segmentLength = beatsPerBar * Divisor * bars_per_segment;
|
|
|
|
if (!IsBeatSyncedWithTrack)
|
|
{
|
|
firstBeat = null;
|
|
return;
|
|
}
|
|
|
|
if (!firstBeat.HasValue || beatIndex < firstBeat)
|
|
// decide on a good starting beat index if once has not yet been decided.
|
|
firstBeat = beatIndex < 0 ? 0 : (beatIndex / segmentLength) * segmentLength;
|
|
|
|
if (beatIndex >= firstBeat)
|
|
playBeatFor(beatIndex, segmentLength, timingPoint);
|
|
}
|
|
|
|
private void playBeatFor(int beatIndex, int segmentLength, TimingControlPoint timingPoint)
|
|
{
|
|
// https://github.com/peppy/osu-stable-reference/blob/6ab0cf1f9f7b3449f5c0d8defcd458aae72cdb88/osu!/Audio/NightcoreBeat.cs#L41
|
|
if (lastBeat == beatIndex)
|
|
return;
|
|
|
|
lastBeat = beatIndex;
|
|
|
|
int beatInSegment = beatIndex % segmentLength;
|
|
|
|
if (beatInSegment == 0)
|
|
{
|
|
// https://github.com/peppy/osu-stable-reference/blob/6ab0cf1f9f7b3449f5c0d8defcd458aae72cdb88/osu!/Audio/NightcoreBeat.cs#L53
|
|
bool playFinish = beatIndex > 0 || !timingPoint.OmitFirstBarLine;
|
|
|
|
if (playFinish)
|
|
finishSample?.Play();
|
|
}
|
|
|
|
switch (timingPoint.TimeSignature.Numerator)
|
|
{
|
|
case 3:
|
|
switch (beatInSegment % 6)
|
|
{
|
|
case 0:
|
|
kickSample?.Play();
|
|
break;
|
|
|
|
case 3:
|
|
clapSample?.Play();
|
|
break;
|
|
|
|
default:
|
|
hatSample?.Play();
|
|
break;
|
|
}
|
|
|
|
break;
|
|
|
|
case 4:
|
|
switch (beatInSegment % 4)
|
|
{
|
|
case 0:
|
|
kickSample?.Play();
|
|
break;
|
|
|
|
case 2:
|
|
clapSample?.Play();
|
|
break;
|
|
|
|
default:
|
|
// note that in stable hat samples would only play if the beatmap tick rate was even
|
|
// (https://github.com/peppy/osu-stable-reference/blob/6ab0cf1f9f7b3449f5c0d8defcd458aae72cdb88/osu!/Audio/NightcoreBeat.cs#L30-L32)
|
|
// that kind of presumes that only music timed in 4/4 exists, and does not really work well
|
|
// if the beatmap e.g. mixes 4/4 and 3/4 signature timing control points.
|
|
// therefore this conditional behaviour is not reimplemented.
|
|
hatSample?.Play();
|
|
break;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|