1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 22:27:25 +08:00
osu-lazer/osu.Game/Screens/Play/MasterGameplayClockContainer.cs

308 lines
12 KiB
C#
Raw Normal View History

2021-04-14 18:50:22 +08:00
// 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 System.Collections.Generic;
2022-03-01 17:29:04 +08:00
using System.ComponentModel;
2021-04-14 18:50:22 +08:00
using System.Linq;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
2022-03-01 15:59:33 +08:00
using osu.Game.Database;
2021-04-14 18:50:22 +08:00
namespace osu.Game.Screens.Play
{
/// <summary>
/// A <see cref="GameplayClockContainer"/> which uses a <see cref="WorkingBeatmap"/> as a source.
/// <para>
/// This is the most complete <see cref="GameplayClockContainer"/> which takes into account all user and platform offsets,
/// and provides implementations for user actions such as skipping or adjusting playback rates that may occur during gameplay.
/// </para>
/// </summary>
/// <remarks>
/// This is intended to be used as a single controller for gameplay, or as a reference source for other <see cref="GameplayClockContainer"/>s.
/// </remarks>
2021-04-14 18:50:22 +08:00
public class MasterGameplayClockContainer : GameplayClockContainer
{
/// <summary>
/// Duration before gameplay start time required before skip button displays.
/// </summary>
public const double MINIMUM_SKIP_TIME = 1000;
2021-04-19 18:55:59 +08:00
protected Track Track => (Track)SourceClock;
2021-04-14 18:50:22 +08:00
public readonly BindableNumber<double> UserPlaybackRate = new BindableDouble(1)
{
Default = 1,
MinValue = 0.5,
MaxValue = 2,
Precision = 0.1,
};
2022-03-01 15:59:33 +08:00
private double totalAppliedOffset => userBeatmapOffsetClock.RateAdjustedOffset + userGlobalOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset;
2021-04-14 18:50:22 +08:00
private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1);
private readonly WorkingBeatmap beatmap;
private readonly double gameplayStartTime;
private readonly bool startAtGameplayStart;
private readonly double firstHitObjectTime;
2022-03-01 15:59:33 +08:00
private HardwareCorrectionOffsetClock userGlobalOffsetClock;
private HardwareCorrectionOffsetClock userBeatmapOffsetClock;
private HardwareCorrectionOffsetClock platformOffsetClock;
2021-04-21 15:33:14 +08:00
private MasterGameplayClock masterGameplayClock;
2021-04-14 18:50:22 +08:00
private Bindable<double> userAudioOffset;
private double startOffset;
2021-04-14 18:50:22 +08:00
public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false)
: base(beatmap.Track)
{
this.beatmap = beatmap;
this.gameplayStartTime = gameplayStartTime;
this.startAtGameplayStart = startAtGameplayStart;
firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime;
}
2022-03-01 17:29:04 +08:00
[Resolved]
private RealmAccess realm { get; set; }
[Resolved]
private OsuConfigManager config { get; set; }
private IDisposable beatmapOffsetSubscription;
protected override void LoadComplete()
2021-04-14 18:50:22 +08:00
{
2022-03-01 17:29:04 +08:00
base.LoadComplete();
2021-04-14 18:50:22 +08:00
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
2022-03-01 15:59:33 +08:00
userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true);
2022-03-01 17:29:04 +08:00
beatmapOffsetSubscription = realm.RegisterCustomSubscription(r =>
{
var userSettings = r.Find<BeatmapInfo>(beatmap.BeatmapInfo.ID)?.UserSettings;
if (userSettings == null) // only the case for tests.
return null;
2022-03-01 17:29:04 +08:00
void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args)
{
if (args.PropertyName == nameof(BeatmapUserSettings.Offset))
updateOffset();
}
updateOffset();
userSettings.PropertyChanged += onUserSettingsOnPropertyChanged;
return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged);
void updateOffset() => userBeatmapOffsetClock.Offset = userSettings.Offset;
});
2021-04-14 18:50:22 +08:00
// sane default provided by ruleset.
startOffset = gameplayStartTime;
2021-04-14 18:50:22 +08:00
if (!startAtGameplayStart)
{
startOffset = Math.Min(0, startOffset);
2021-04-14 18:50:22 +08:00
// if a storyboard is present, it may dictate the appropriate start time by having events in negative time space.
// this is commonly used to display an intro before the audio track start.
double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime;
if (firstStoryboardEvent != null)
startOffset = Math.Min(startOffset, firstStoryboardEvent.Value);
2021-04-14 18:50:22 +08:00
// some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available.
// this is not available as an option in the live editor but can still be applied via .osu editing.
if (beatmap.BeatmapInfo.AudioLeadIn > 0)
startOffset = Math.Min(startOffset, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn);
2021-04-14 18:50:22 +08:00
}
Seek(startOffset);
2021-04-14 18:50:22 +08:00
}
protected override void OnIsPausedChanged(ValueChangedEvent<bool> isPaused)
{
2021-04-16 19:33:29 +08:00
// The source is stopped by a frequency fade first.
2021-04-14 18:50:22 +08:00
if (isPaused.NewValue)
{
this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ =>
{
if (IsPaused.Value == isPaused.NewValue)
AdjustableSource.Stop();
});
}
2021-04-14 18:50:22 +08:00
else
this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In);
}
public override void Start()
{
addSourceClockAdjustments();
base.Start();
}
2021-04-16 19:33:29 +08:00
/// <summary>
/// Seek to a specific time in gameplay.
/// </summary>
/// <remarks>
/// Adjusts for any offsets which have been applied (so the seek may not be the expected point in time on the underlying audio track).
/// </remarks>
/// <param name="time">The destination time to seek to.</param>
2021-04-14 18:50:22 +08:00
public override void Seek(double time)
{
// remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track.
// we may want to consider reversing the application of offsets in the future as it may feel more correct.
base.Seek(time - totalAppliedOffset);
2021-04-14 18:50:22 +08:00
}
/// <summary>
/// Skip forward to the next valid skip point.
/// </summary>
public void Skip()
{
if (GameplayClock.CurrentTime > gameplayStartTime - MINIMUM_SKIP_TIME)
return;
double skipTarget = gameplayStartTime - MINIMUM_SKIP_TIME;
if (GameplayClock.CurrentTime < 0 && skipTarget > 6000)
// double skip exception for storyboards with very long intros
skipTarget = 0;
Seek(skipTarget);
}
public override void Reset()
{
base.Reset();
Seek(startOffset);
}
2021-04-14 20:15:14 +08:00
protected override GameplayClock CreateGameplayClock(IFrameBasedClock source)
2021-04-14 18:50:22 +08:00
{
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
platformOffsetClock = new HardwareCorrectionOffsetClock(source, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
2021-04-14 18:50:22 +08:00
// the final usable gameplay clock with user-set offsets applied.
2022-03-01 15:59:33 +08:00
userGlobalOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock, pauseFreqAdjust);
userBeatmapOffsetClock = new HardwareCorrectionOffsetClock(userGlobalOffsetClock, pauseFreqAdjust);
2021-04-14 18:50:22 +08:00
2022-03-01 15:59:33 +08:00
return masterGameplayClock = new MasterGameplayClock(userBeatmapOffsetClock);
2021-04-14 18:50:22 +08:00
}
/// <summary>
/// Changes the backing clock to avoid using the originally provided track.
/// </summary>
public void StopUsingBeatmapClock()
{
removeSourceClockAdjustments();
2021-04-19 18:55:59 +08:00
ChangeSource(new TrackVirtual(beatmap.Track.Length));
addSourceClockAdjustments();
2021-04-14 18:50:22 +08:00
}
private bool speedAdjustmentsApplied;
private void addSourceClockAdjustments()
2021-04-14 18:50:22 +08:00
{
if (speedAdjustmentsApplied)
return;
Track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
Track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
2021-04-21 15:33:14 +08:00
masterGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust);
masterGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate);
2021-04-14 18:50:22 +08:00
speedAdjustmentsApplied = true;
}
private void removeSourceClockAdjustments()
{
if (!speedAdjustmentsApplied)
return;
Track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
Track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
2021-04-21 15:33:14 +08:00
masterGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust);
masterGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate);
2021-04-14 18:50:22 +08:00
speedAdjustmentsApplied = false;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
2022-03-01 17:29:04 +08:00
beatmapOffsetSubscription?.Dispose();
2021-04-14 18:50:22 +08:00
removeSourceClockAdjustments();
}
private class HardwareCorrectionOffsetClock : FramedOffsetClock
{
private readonly BindableDouble pauseRateAdjust;
private double offset;
public new double Offset
{
get => offset;
set
{
if (value == offset)
return;
offset = value;
updateOffset();
}
}
public double RateAdjustedOffset => base.Offset;
public HardwareCorrectionOffsetClock(IClock source, BindableDouble pauseRateAdjust)
: base(source)
{
this.pauseRateAdjust = pauseRateAdjust;
}
public override void ProcessFrame()
2021-04-14 18:50:22 +08:00
{
base.ProcessFrame();
updateOffset();
}
private void updateOffset()
{
// changing this during the pause transform effect will cause a potentially large offset to be suddenly applied as we approach zero rate.
if (pauseRateAdjust.Value == 1)
{
// we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this.
base.Offset = Offset * Rate;
}
2021-04-14 18:50:22 +08:00
}
}
2021-04-21 15:33:14 +08:00
private class MasterGameplayClock : GameplayClock
2021-04-14 18:50:22 +08:00
{
public readonly List<Bindable<double>> MutableNonGameplayAdjustments = new List<Bindable<double>>();
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => MutableNonGameplayAdjustments;
2021-04-21 15:33:14 +08:00
public MasterGameplayClock(FramedOffsetClock underlyingClock)
2021-04-14 18:50:22 +08:00
: base(underlyingClock)
{
}
}
}
}