1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 02:49:53 +08:00
Files
osu-lazer/osu.Game/Screens/Play/ReplayFailIndicator.cs
T
Bartłomiej Dach 59ec6ed2eb Stop fail sample when rewinding to before it in replay
closes https://github.com/ppy/osu/issues/34688

I originally wrote it this way semi-intentionally because I thought
cutting out the sample was worse than letting it play out, but I also
forgot that people use like seventy hour long fail samples.
2025-08-18 09:02:49 +02:00

175 lines
7.0 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 System;
using ManagedBass.Fx;
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.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Platform;
using osu.Game.Audio;
using osu.Game.Audio.Effects;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Skinning;
using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Screens.Play
{
public partial class ReplayFailIndicator : CompositeDrawable
{
public Action? GoToResults { get; init; }
private readonly GameplayClockContainer gameplayClockContainer;
private readonly BindableDouble trackFreq = new BindableDouble(1);
private readonly BindableDouble volumeAdjustment = new BindableDouble(1);
private Track track = null!;
private SkinnableSound failSample = null!;
private AudioFilter failLowPassFilter = null!;
private AudioFilter failHighPassFilter = null!;
private Container content = null!;
private double? failTime;
// relied on to make arbitrary seeks / rewinding work pretty well out-of-the-box, leveraging custom clock and absolute transform sequences
public override bool RemoveCompletedTransforms => false;
public ReplayFailIndicator(GameplayClockContainer gameplayClockContainer)
{
Clock = this.gameplayClockContainer = gameplayClockContainer;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, AudioManager audio, IBindable<WorkingBeatmap> beatmap, GameHost host)
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
track = beatmap.Value.Track;
RoundedButton goToResultsButton;
InternalChildren = new Drawable[]
{
failSample = new SkinnableSound(new SampleInfo(@"Gameplay/failsound")),
failLowPassFilter = new AudioFilter(audio.TrackMixer),
failHighPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
content = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 20,
Alpha = 0,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray3,
Alpha = 0.8f,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding(20),
Spacing = new Vector2(15),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Style.Title,
Text = ReplayFailIndicatorStrings.ReplayFailed,
},
goToResultsButton = new RoundedButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 150,
Text = ReplayFailIndicatorStrings.GoToResults,
Action = GoToResults,
}
}
}
}
}
};
// every single component here is fine being synced to the gameplay clock...
// except the "go to results" button, which starts having hover animations synced to the audio track
// which is something that we don't want.
// it is maybe probably possible to restructure the drawable hierarchy here to remove the button from under the gameplay clock,
// but it would resort in uglier and more complicated drawable code.
// thus, resort to the escape hatch extension method to ensure the button specifically still runs on the game update clock.
goToResultsButton.ApplyGameWideClock(host);
track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
track.AddAdjustment(AdjustableProperty.Frequency, trackFreq);
}
public void Display()
{
failTime = Clock.CurrentTime;
using (BeginAbsoluteSequence(failTime.Value))
{
// intentionally shorter than the actual fail animation
const double audio_sweep_duration = 1000;
content.FadeInFromZero(200, Easing.OutQuint);
this.ScaleTo(1.1f, audio_sweep_duration, Easing.OutElasticHalf);
this.TransformBindableTo(trackFreq, 0, audio_sweep_duration);
this.TransformBindableTo(volumeAdjustment, 0.5);
failHighPassFilter.CutoffTo(300);
failLowPassFilter.CutoffTo(300, audio_sweep_duration, Easing.OutCubic);
}
}
private bool failSamplePlaybackInitiated;
protected override void Update()
{
base.Update();
// the playback of the fail sample is the one thing that cannot be easily written using rewindable transforms and such.
// this part needs to be hardcoded in update to work.
if (gameplayClockContainer.GetTrueGameplayRate() > 0 && Time.Current >= failTime && !failSamplePlaybackInitiated)
{
failSamplePlaybackInitiated = true;
failSample.Play();
}
if (Time.Current < failTime && failSamplePlaybackInitiated)
{
failSamplePlaybackInitiated = false;
failSample.Stop();
}
}
protected override void Dispose(bool isDisposing)
{
failSample.Stop();
failSample.Dispose();
track.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
base.Dispose(isDisposing);
}
}
}