Merge branch 'master' into extract-slider-tick-creation

This commit is contained in:
Dean Herbert 2019-03-08 20:00:12 +09:00 committed by GitHub
commit f59b9e933e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 376 additions and 259 deletions

View File

@ -16,18 +16,18 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestFixture] [TestFixture]
public class TestCaseGameplayCursor : OsuTestCase, IProvideCursor public class TestCaseGameplayCursor : OsuTestCase, IProvideCursor
{ {
private GameplayCursor cursor; private GameplayCursorContainer cursorContainer;
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(CursorTrail) }; public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(CursorTrail) };
public CursorContainer Cursor => cursor; public CursorContainer Cursor => cursorContainer;
public bool ProvidingUserCursor => true; public bool ProvidingUserCursor => true;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Add(cursor = new GameplayCursor { RelativeSizeAxes = Axes.Both }); Add(cursorContainer = new GameplayCursorContainer { RelativeSizeAxes = Axes.Both });
} }
} }
} }

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Cursor;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -16,8 +15,14 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
} }
protected override CursorContainer CreateCursor() => null; protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor { Size = Vector2.One };
protected override Playfield CreatePlayfield() => new OsuPlayfield { Size = Vector2.One }; private class OsuPlayfieldNoCursor : OsuPlayfield
{
public OsuPlayfieldNoCursor()
{
Cursor?.Expire();
}
}
} }
} }

View File

@ -17,7 +17,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.UI.Cursor namespace osu.Game.Rulesets.Osu.UI.Cursor
{ {
public class GameplayCursor : CursorContainer, IKeyBindingHandler<OsuAction> public class GameplayCursorContainer : CursorContainer, IKeyBindingHandler<OsuAction>
{ {
protected override Drawable CreateCursor() => new OsuCursor(); protected override Drawable CreateCursor() => new OsuCursor();
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly Container<Drawable> fadeContainer; private readonly Container<Drawable> fadeContainer;
public GameplayCursor() public GameplayCursorContainer()
{ {
InternalChild = fadeContainer = new Container InternalChild = fadeContainer = new Container
{ {
@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public OsuCursor() public OsuCursor()
{ {
Origin = Anchor.Centre; Origin = Anchor.Centre;
Size = new Vector2(42); Size = new Vector2(28);
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin, bool allowFallback)

View File

@ -10,7 +10,9 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using System.Linq; using System.Linq;
using osu.Framework.Graphics.Cursor;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.UI.Cursor;
namespace osu.Game.Rulesets.Osu.UI namespace osu.Game.Rulesets.Osu.UI
{ {
@ -22,6 +24,12 @@ namespace osu.Game.Rulesets.Osu.UI
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
private readonly PlayfieldAdjustmentContainer adjustmentContainer;
protected override Container CursorTargetContainer => adjustmentContainer;
protected override CursorContainer CreateCursor() => new GameplayCursorContainer();
public OsuPlayfield() public OsuPlayfield()
{ {
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
@ -29,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.UI
Size = new Vector2(0.75f); Size = new Vector2(0.75f);
InternalChild = new PlayfieldAdjustmentContainer InternalChild = adjustmentContainer = new PlayfieldAdjustmentContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq; using System.Linq;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
@ -13,7 +12,6 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -59,7 +57,5 @@ namespace osu.Game.Rulesets.Osu.UI
return first.StartTime - first.TimePreempt; return first.StartTime - first.TimePreempt;
} }
} }
protected override CursorContainer CreateCursor() => new GameplayCursor();
} }
} }

View File

@ -24,10 +24,13 @@ namespace osu.Game.Tests.Visual
public TestCaseToolbar() public TestCaseToolbar()
{ {
var toolbar = new Toolbar { State = Visibility.Visible }; var toolbar = new Toolbar { State = Visibility.Visible };
ToolbarNotificationButton notificationButton = null;
AddStep("create toolbar", () =>
{
Add(toolbar); Add(toolbar);
notificationButton = toolbar.Children.OfType<FillFlowContainer>().Last().Children.OfType<ToolbarNotificationButton>().First();
var notificationButton = toolbar.Children.OfType<FillFlowContainer>().Last().Children.OfType<ToolbarNotificationButton>().First(); });
void setNotifications(int count) => AddStep($"set notification count to {count}", () => notificationButton.NotificationCount.Value = count); void setNotifications(int count) => AddStep($"set notification count to {count}", () => notificationButton.NotificationCount.Value = count);

View File

@ -70,13 +70,15 @@ namespace osu.Game.Online.API
internal new void Schedule(Action action) => base.Schedule(action); internal new void Schedule(Action action) => base.Schedule(action);
/// <summary>
/// Register a component to receive API events.
/// Fires <see cref="IOnlineComponent.APIStateChanged"/> once immediately to ensure a correct state.
/// </summary>
/// <param name="component"></param>
public void Register(IOnlineComponent component) public void Register(IOnlineComponent component)
{ {
Scheduler.Add(delegate Scheduler.Add(delegate { components.Add(component); });
{
components.Add(component);
component.APIStateChanged(this, state); component.APIStateChanged(this, state);
});
} }
public void Unregister(IOnlineComponent component) public void Unregister(IOnlineComponent component)

View File

@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Toolbar
public Action OnHome; public Action OnHome;
private readonly ToolbarUserArea userArea; private ToolbarUserArea userArea;
protected override bool BlockPositionalInput => false; protected override bool BlockPositionalInput => false;
@ -34,6 +34,13 @@ namespace osu.Game.Overlays.Toolbar
private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All); private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
public Toolbar() public Toolbar()
{
RelativeSizeAxes = Axes.X;
Size = new Vector2(1, HEIGHT);
}
[BackgroundDependencyLoader(true)]
private void load(OsuGame osuGame)
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
@ -76,13 +83,6 @@ namespace osu.Game.Overlays.Toolbar
} }
}; };
RelativeSizeAxes = Axes.X;
Size = new Vector2(1, HEIGHT);
}
[BackgroundDependencyLoader(true)]
private void load(OsuGame osuGame)
{
StateChanged += visibility => StateChanged += visibility =>
{ {
if (overlayActivationMode.Value == OverlayActivation.Disabled) if (overlayActivationMode.Value == OverlayActivation.Disabled)

View File

@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osuTK; using osuTK;
@ -63,6 +64,10 @@ namespace osu.Game.Rulesets.UI
private void load(IBindable<WorkingBeatmap> beatmap) private void load(IBindable<WorkingBeatmap> beatmap)
{ {
this.beatmap = beatmap.Value; this.beatmap = beatmap.Value;
Cursor = CreateCursor();
if (Cursor != null)
CursorTargetContainer.Add(Cursor);
} }
/// <summary> /// <summary>
@ -82,6 +87,23 @@ namespace osu.Game.Rulesets.UI
/// <param name="h">The DrawableHitObject to remove.</param> /// <param name="h">The DrawableHitObject to remove.</param>
public virtual bool Remove(DrawableHitObject h) => HitObjectContainer.Remove(h); public virtual bool Remove(DrawableHitObject h) => HitObjectContainer.Remove(h);
/// <summary>
/// The cursor currently being used by this <see cref="Playfield"/>. May be null if no cursor is provided.
/// </summary>
public CursorContainer Cursor { get; private set; }
/// <summary>
/// Provide an optional cursor which is to be used for gameplay.
/// If providing a cursor, <see cref="CursorTargetContainer"/> must also point to a valid target container.
/// </summary>
/// <returns>The cursor, or null if a cursor is not rqeuired.</returns>
protected virtual CursorContainer CreateCursor() => null;
/// <summary>
/// The target container to add the cursor after it is created.
/// </summary>
protected virtual Container CursorTargetContainer => null;
/// <summary> /// <summary>
/// Registers a <see cref="Playfield"/> as a nested <see cref="Playfield"/>. /// Registers a <see cref="Playfield"/> as a nested <see cref="Playfield"/>.
/// This does not add the <see cref="Playfield"/> to the draw hierarchy. /// This does not add the <see cref="Playfield"/> to the draw hierarchy.

View File

@ -16,7 +16,9 @@ using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Cursor;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Replays; using osu.Game.Replays;
@ -32,7 +34,7 @@ namespace osu.Game.Rulesets.UI
/// Should not be derived - derive <see cref="RulesetContainer{TObject}"/> instead. /// Should not be derived - derive <see cref="RulesetContainer{TObject}"/> instead.
/// </para> /// </para>
/// </summary> /// </summary>
public abstract class RulesetContainer : Container public abstract class RulesetContainer : Container, IProvideCursor
{ {
/// <summary> /// <summary>
/// The selected variant. /// The selected variant.
@ -74,10 +76,11 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
public Container Overlays { get; protected set; } public Container Overlays { get; protected set; }
/// <summary> public CursorContainer Cursor => Playfield.Cursor;
/// The cursor provided by this <see cref="RulesetContainer"/>. May be null if no cursor is provided.
/// </summary> public bool ProvidingUserCursor => Playfield.Cursor != null && !HasReplayLoaded.Value;
public readonly CursorContainer Cursor;
protected override bool OnHover(HoverEvent e) => true; // required for IProvideCursor
public readonly Ruleset Ruleset; public readonly Ruleset Ruleset;
@ -101,8 +104,6 @@ namespace osu.Game.Rulesets.UI
KeyBindingInputManager.UseParentInput = !paused.NewValue; KeyBindingInputManager.UseParentInput = !paused.NewValue;
}; };
Cursor = CreateCursor();
} }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -259,9 +260,6 @@ namespace osu.Game.Rulesets.UI
Playfield Playfield
}); });
if (Cursor != null)
KeyBindingInputManager.Add(Cursor);
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
KeyBindingInputManager, KeyBindingInputManager,

View File

@ -107,6 +107,14 @@ namespace osu.Game.Screens.Multi.Lounge
Filter.Search.HoldFocus = false; Filter.Search.HoldFocus = false;
} }
public override void OnResuming(IScreen last)
{
base.OnResuming(last);
if (currentRoom.Value?.RoomID.Value == null)
currentRoom.Value = new Room();
}
private void joinRequested(Room room) private void joinRequested(Room room)
{ {
processingOverlay.Show(); processingOverlay.Show();

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -13,6 +14,7 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Overlays.SearchableList; using osu.Game.Overlays.SearchableList;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osuTK; using osuTK;
@ -108,7 +110,7 @@ namespace osu.Game.Screens.Multi.Match.Components
}, },
}; };
CurrentItem.BindValueChanged(item => modDisplay.Current.Value = item.NewValue?.RequiredMods, true); CurrentItem.BindValueChanged(item => modDisplay.Current.Value = item.NewValue?.RequiredMods ?? Enumerable.Empty<Mod>(), true);
beatmapButton.Action = () => RequestBeatmapSelection?.Invoke(); beatmapButton.Action = () => RequestBeatmapSelection?.Invoke();
} }

View File

@ -0,0 +1,151 @@
// 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.Linq;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Screens.Play
{
/// <summary>
/// Encapsulates gameplay timing logic and provides a <see cref="GameplayClock"/> for children.
/// </summary>
public class GameplayClockContainer : Container
{
private readonly WorkingBeatmap beatmap;
/// <summary>
/// The original source (usually a <see cref="WorkingBeatmap"/>'s track).
/// </summary>
private readonly IAdjustableClock sourceClock;
public readonly BindableBool IsPaused = new BindableBool();
/// <summary>
/// The decoupled clock used for gameplay. Should be used for seeks and clock control.
/// </summary>
private readonly DecoupleableInterpolatingFramedClock adjustableClock;
public readonly Bindable<double> UserPlaybackRate = new BindableDouble(1)
{
Default = 1,
MinValue = 0.5,
MaxValue = 2,
Precision = 0.1,
};
/// <summary>
/// The final clock which is exposed to underlying components.
/// </summary>
[Cached]
private readonly GameplayClock gameplayClock;
private Bindable<double> userAudioOffset;
private readonly FramedOffsetClock offsetClock;
public GameplayClockContainer(WorkingBeatmap beatmap, bool allowLeadIn, double gameplayStartTime)
{
this.beatmap = beatmap;
RelativeSizeAxes = Axes.Both;
sourceClock = (IAdjustableClock)beatmap.Track ?? new StopwatchClock();
adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
adjustableClock.Seek(allowLeadIn
? Math.Min(0, gameplayStartTime - beatmap.BeatmapInfo.AudioLeadIn)
: gameplayStartTime);
adjustableClock.ProcessFrame();
// 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.
var platformOffsetClock = new FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 22 : 0 };
// the final usable gameplay clock with user-set offsets applied.
offsetClock = new FramedOffsetClock(platformOffsetClock);
// the clock to be exposed via DI to children.
gameplayClock = new GameplayClock(offsetClock);
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
userAudioOffset.BindValueChanged(offset => offsetClock.Offset = offset.NewValue, true);
UserPlaybackRate.ValueChanged += _ => updateRate();
}
public void Restart()
{
Task.Run(() =>
{
sourceClock.Reset();
Schedule(() =>
{
adjustableClock.ChangeSource(sourceClock);
updateRate();
this.Delay(750).Schedule(() =>
{
if (!IsPaused.Value)
{
adjustableClock.Start();
}
});
});
});
}
public void Start()
{
// Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
// This accounts for the audio clock source potentially taking time to enter a completely stopped state
adjustableClock.Seek(adjustableClock.CurrentTime);
adjustableClock.Start();
}
public void Seek(double time) => adjustableClock.Seek(time);
public void Stop() => adjustableClock.Stop();
public void ResetLocalAdjustments()
{
// In the case of replays, we may have changed the playback rate.
UserPlaybackRate.Value = 1;
}
protected override void Update()
{
if (!IsPaused.Value)
offsetClock.ProcessFrame();
base.Update();
}
private void updateRate()
{
if (sourceClock == null) return;
sourceClock.Rate = 1;
foreach (var mod in beatmap.Mods.Value.OfType<IApplicableToClock>())
mod.ApplyToClock(sourceClock);
sourceClock.Rate *= UserPlaybackRate.Value;
}
}
}

View File

@ -19,7 +19,9 @@ namespace osu.Game.Screens.Play.HUD
public readonly PlaybackSettings PlaybackSettings; public readonly PlaybackSettings PlaybackSettings;
public readonly VisualSettings VisualSettings; public readonly VisualSettings VisualSettings;
//public readonly CollectionSettings CollectionSettings; //public readonly CollectionSettings CollectionSettings;
//public readonly DiscussionSettings DiscussionSettings; //public readonly DiscussionSettings DiscussionSettings;
public PlayerSettingsOverlay() public PlayerSettingsOverlay()

View File

@ -1,12 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -40,7 +40,9 @@ namespace osu.Game.Screens.Play
private static bool hasShownNotificationOnce; private static bool hasShownNotificationOnce;
public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IAdjustableClock adjustableClock) public Action<double> RequestSeek;
public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working)
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -92,11 +94,9 @@ namespace osu.Game.Screens.Play
Progress.Objects = rulesetContainer.Objects; Progress.Objects = rulesetContainer.Objects;
Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value; Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value;
Progress.RequestSeek = pos => adjustableClock.Seek(pos); Progress.RequestSeek = time => RequestSeek(time);
ModDisplay.Current.BindTo(working.Mods); ModDisplay.Current.BindTo(working.Mods);
PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableClock;
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]

View File

@ -7,16 +7,13 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
/// <summary> /// <summary>
/// A container which handles pausing children, displaying a pause overlay with choices and processing the clock. /// A container which handles pausing children, displaying an overlay blocking its children during paused state.
/// Exposes a <see cref="GameplayClock"/> to children via DI.
/// This alleviates a lot of the intricate pause logic from being in <see cref="Player"/>
/// </summary> /// </summary>
public class PausableGameplayContainer : Container public class PausableGameplayContainer : Container
{ {
@ -44,37 +41,23 @@ namespace osu.Game.Screens.Play
public Action OnRetry; public Action OnRetry;
public Action OnQuit; public Action OnQuit;
private readonly FramedClock offsetClock; public Action Stop;
private readonly DecoupleableInterpolatingFramedClock adjustableClock; public Action Start;
/// <summary>
/// The final clock which is exposed to underlying components.
/// </summary>
[Cached]
private readonly GameplayClock gameplayClock;
/// <summary> /// <summary>
/// Creates a new <see cref="PausableGameplayContainer"/>. /// Creates a new <see cref="PausableGameplayContainer"/>.
/// </summary> /// </summary>
/// <param name="offsetClock">The gameplay clock. This is the clock that will process frames. Includes user/system offsets.</param> public PausableGameplayContainer()
/// <param name="adjustableClock">The seekable clock. This is the clock that will be paused and resumed. Should not be processed (it is processed automatically by <see cref="offsetClock"/>).</param>
public PausableGameplayContainer(FramedClock offsetClock, DecoupleableInterpolatingFramedClock adjustableClock)
{ {
this.offsetClock = offsetClock;
this.adjustableClock = adjustableClock;
gameplayClock = new GameplayClock(offsetClock);
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
AddInternal(content = new Container InternalChildren = new[]
{
content = new Container
{ {
Clock = this.offsetClock,
ProcessCustomClock = false,
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}); },
pauseOverlay = new PauseOverlay
AddInternal(pauseOverlay = new PauseOverlay
{ {
OnResume = () => OnResume = () =>
{ {
@ -83,7 +66,8 @@ namespace osu.Game.Screens.Play
}, },
OnRetry = () => OnRetry(), OnRetry = () => OnRetry(),
OnQuit = () => OnQuit(), OnQuit = () => OnQuit(),
}); }
};
} }
public void Pause(bool force = false) => Schedule(() => // Scheduled to ensure a stable position in execution order, no matter how it was called. public void Pause(bool force = false) => Schedule(() => // Scheduled to ensure a stable position in execution order, no matter how it was called.
@ -93,7 +77,7 @@ namespace osu.Game.Screens.Play
if (IsPaused.Value) return; if (IsPaused.Value) return;
// stop the seekable clock (stops the audio eventually) // stop the seekable clock (stops the audio eventually)
adjustableClock.Stop(); Stop?.Invoke();
IsPaused.Value = true; IsPaused.Value = true;
pauseOverlay.Show(); pauseOverlay.Show();
@ -105,14 +89,12 @@ namespace osu.Game.Screens.Play
{ {
if (!IsPaused.Value) return; if (!IsPaused.Value) return;
IsPaused.Value = false;
IsResuming = false; IsResuming = false;
lastPauseActionTime = Time.Current; lastPauseActionTime = Time.Current;
// Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time IsPaused.Value = false;
// This accounts for the audio clock source potentially taking time to enter a completely stopped state
adjustableClock.Seek(adjustableClock.CurrentTime); Start?.Invoke();
adjustableClock.Start();
pauseOverlay.Hide(); pauseOverlay.Hide();
} }
@ -131,9 +113,6 @@ namespace osu.Game.Screens.Play
if (!game.IsActive.Value && CanPause) if (!game.IsActive.Value && CanPause)
Pause(); Pause();
if (!IsPaused.Value)
offsetClock.ProcessFrame();
base.Update(); base.Update();
} }

View File

@ -3,24 +3,19 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -34,7 +29,7 @@ using osu.Game.Storyboards.Drawables;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
public class Player : ScreenWithBeatmapBackground, IProvideCursor public class Player : ScreenWithBeatmapBackground
{ {
protected override bool AllowBackButton => false; // handled by HoldForMenuButton protected override bool AllowBackButton => false; // handled by HoldForMenuButton
@ -53,22 +48,11 @@ namespace osu.Game.Screens.Play
public bool AllowResults { get; set; } = true; public bool AllowResults { get; set; } = true;
private Bindable<bool> mouseWheelDisabled; private Bindable<bool> mouseWheelDisabled;
private Bindable<double> userAudioOffset;
private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>(); private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>();
public int RestartCount; public int RestartCount;
public CursorContainer Cursor => RulesetContainer.Cursor;
public bool ProvidingUserCursor => RulesetContainer?.Cursor != null && !RulesetContainer.HasReplayLoaded.Value;
private IAdjustableClock sourceClock;
/// <summary>
/// The decoupled clock used for gameplay. Should be used for seeks and clock control.
/// </summary>
private DecoupleableInterpolatingFramedClock adjustableClock;
[Resolved] [Resolved]
private ScoreManager scoreManager { get; set; } private ScoreManager scoreManager { get; set; }
@ -98,25 +82,113 @@ namespace osu.Game.Screens.Play
public bool LoadedBeatmapSuccessfully => RulesetContainer?.Objects.Any() == true; public bool LoadedBeatmapSuccessfully => RulesetContainer?.Objects.Any() == true;
private GameplayClockContainer gameplayClockContainer;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, APIAccess api, OsuConfigManager config) private void load(AudioManager audio, APIAccess api, OsuConfigManager config)
{ {
this.api = api; this.api = api;
WorkingBeatmap working = Beatmap.Value; WorkingBeatmap working = loadBeatmap();
if (working is DummyWorkingBeatmap)
if (working == null)
return; return;
sampleRestart = audio.Sample.Get(@"Gameplay/restart"); sampleRestart = audio.Sample.Get(@"Gameplay/restart");
mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel); mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
IBeatmap beatmap; ScoreProcessor = RulesetContainer.CreateScoreProcessor();
if (!ScoreProcessor.Mode.Disabled)
config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
InternalChild = gameplayClockContainer = new GameplayClockContainer(working, AllowLeadIn, RulesetContainer.GameplayStartTime);
gameplayClockContainer.Children = new Drawable[]
{
PausableGameplayContainer = new PausableGameplayContainer
{
Retries = RestartCount,
OnRetry = restart,
OnQuit = performUserRequestedExit,
Start = gameplayClockContainer.Start,
Stop = gameplayClockContainer.Stop,
IsPaused = { BindTarget = gameplayClockContainer.IsPaused },
CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value,
Children = new[]
{
StoryboardContainer = CreateStoryboardContainer(),
new ScalingContainer(ScalingMode.Gameplay)
{
Child = new LocalSkinOverrideContainer(working.Skin)
{
RelativeSizeAxes = Axes.Both,
Child = RulesetContainer
}
},
new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Breaks = working.Beatmap.Breaks
},
// display the cursor above some HUD elements.
RulesetContainer.Cursor?.CreateProxy() ?? new Container(),
HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working)
{
HoldToQuit = { Action = performUserRequestedExit },
PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = gameplayClockContainer.UserPlaybackRate } } },
KeyCounter = { Visible = { BindTarget = RulesetContainer.HasReplayLoaded } },
RequestSeek = gameplayClockContainer.Seek,
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
new SkipOverlay(RulesetContainer.GameplayStartTime)
{
RequestSeek = gameplayClockContainer.Seek
},
}
},
failOverlay = new FailOverlay
{
OnRetry = restart,
OnQuit = performUserRequestedExit,
},
new HotkeyRetryOverlay
{
Action = () =>
{
if (!this.IsCurrentScreen()) return;
fadeOut(true);
restart();
},
}
};
// bind clock into components that require it
RulesetContainer.IsPaused.BindTo(gameplayClockContainer.IsPaused);
if (ShowStoryboard.Value)
initializeStoryboard(false);
// Bind ScoreProcessor to ourselves
ScoreProcessor.AllJudged += onCompletion;
ScoreProcessor.Failed += onFail;
foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToScoreProcessor>())
mod.ApplyToScoreProcessor(ScoreProcessor);
}
private WorkingBeatmap loadBeatmap()
{
WorkingBeatmap working = Beatmap.Value;
if (working is DummyWorkingBeatmap)
return null;
try try
{ {
beatmap = working.Beatmap; var beatmap = working.Beatmap;
if (beatmap == null) if (beatmap == null)
throw new InvalidOperationException("Beatmap was not loaded"); throw new InvalidOperationException("Beatmap was not loaded");
@ -140,119 +212,17 @@ namespace osu.Game.Screens.Play
if (!RulesetContainer.Objects.Any()) if (!RulesetContainer.Objects.Any())
{ {
Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error); Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error);
return; return null;
} }
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error(e, "Could not load beatmap sucessfully!"); Logger.Error(e, "Could not load beatmap sucessfully!");
//couldn't load, hard abort! //couldn't load, hard abort!
return; return null;
} }
sourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock(); return working;
adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
adjustableClock.Seek(AllowLeadIn
? Math.Min(0, RulesetContainer.GameplayStartTime - beatmap.BeatmapInfo.AudioLeadIn)
: RulesetContainer.GameplayStartTime);
adjustableClock.ProcessFrame();
// 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.
var platformOffsetClock = new FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 22 : 0 };
// the final usable gameplay clock with user-set offsets applied.
var offsetClock = new FramedOffsetClock(platformOffsetClock);
userAudioOffset.ValueChanged += offset => offsetClock.Offset = offset.NewValue;
userAudioOffset.TriggerChange();
ScoreProcessor = RulesetContainer.CreateScoreProcessor();
if (!ScoreProcessor.Mode.Disabled)
config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
InternalChildren = new Drawable[]
{
PausableGameplayContainer = new PausableGameplayContainer(offsetClock, adjustableClock)
{
Retries = RestartCount,
OnRetry = restart,
OnQuit = performUserRequestedExit,
CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value,
Children = new Container[]
{
StoryboardContainer = CreateStoryboardContainer(),
new ScalingContainer(ScalingMode.Gameplay)
{
Child = new LocalSkinOverrideContainer(working.Skin)
{
RelativeSizeAxes = Axes.Both,
Child = RulesetContainer
}
},
new BreakOverlay(beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Breaks = beatmap.Breaks
},
new ScalingContainer(ScalingMode.Gameplay)
{
Child = RulesetContainer.Cursor?.CreateProxy() ?? new Container(),
},
HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working, adjustableClock)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
new SkipOverlay(RulesetContainer.GameplayStartTime)
{
RequestSeek = time => adjustableClock.Seek(time)
},
}
},
failOverlay = new FailOverlay
{
OnRetry = restart,
OnQuit = performUserRequestedExit,
},
new HotkeyRetryOverlay
{
Action = () =>
{
if (!this.IsCurrentScreen()) return;
fadeOut(true);
restart();
},
}
};
HUDOverlay.HoldToQuit.Action = performUserRequestedExit;
HUDOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded);
RulesetContainer.IsPaused.BindTo(PausableGameplayContainer.IsPaused);
if (ShowStoryboard.Value)
initializeStoryboard(false);
// Bind ScoreProcessor to ourselves
ScoreProcessor.AllJudged += onCompletion;
ScoreProcessor.Failed += onFail;
foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToScoreProcessor>())
mod.ApplyToScoreProcessor(ScoreProcessor);
}
private void applyRateFromMods()
{
if (sourceClock == null) return;
sourceClock.Rate = 1;
foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToClock>())
mod.ApplyToClock(sourceClock);
} }
private void performUserRequestedExit() private void performUserRequestedExit()
@ -321,7 +291,7 @@ namespace osu.Game.Screens.Play
if (Beatmap.Value.Mods.Value.OfType<IApplicableFailOverride>().Any(m => !m.AllowFail)) if (Beatmap.Value.Mods.Value.OfType<IApplicableFailOverride>().Any(m => !m.AllowFail))
return false; return false;
adjustableClock.Stop(); gameplayClockContainer.Stop();
HasFailed = true; HasFailed = true;
failOverlay.Retries = RestartCount; failOverlay.Retries = RestartCount;
@ -355,24 +325,7 @@ namespace osu.Game.Screens.Play
storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable; storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
Task.Run(() => gameplayClockContainer.Restart();
{
sourceClock.Reset();
Schedule(() =>
{
adjustableClock.ChangeSource(sourceClock);
applyRateFromMods();
this.Delay(750).Schedule(() =>
{
if (!PausableGameplayContainer.IsPaused.Value)
{
adjustableClock.Start();
}
});
});
});
PausableGameplayContainer.Alpha = 0; PausableGameplayContainer.Alpha = 0;
PausableGameplayContainer.FadeIn(750, Easing.OutQuint); PausableGameplayContainer.FadeIn(750, Easing.OutQuint);
@ -395,8 +348,8 @@ namespace osu.Game.Screens.Play
if ((!AllowPause || HasFailed || !ValidForResume || PausableGameplayContainer?.IsPaused.Value != false || RulesetContainer?.HasReplayLoaded.Value != false) && (!PausableGameplayContainer?.IsResuming ?? true)) if ((!AllowPause || HasFailed || !ValidForResume || PausableGameplayContainer?.IsPaused.Value != false || RulesetContainer?.HasReplayLoaded.Value != false) && (!PausableGameplayContainer?.IsResuming ?? true))
{ {
// In the case of replays, we may have changed the playback rate. gameplayClockContainer.ResetLocalAdjustments();
applyRateFromMods();
fadeOut(); fadeOut();
return base.OnExiting(next); return base.OnExiting(next);
} }

View File

@ -4,7 +4,6 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -16,7 +15,13 @@ namespace osu.Game.Screens.Play.PlayerSettings
protected override string Title => @"playback"; protected override string Title => @"playback";
public IAdjustableClock AdjustableClock { set; get; } public readonly Bindable<double> UserPlaybackRate = new BindableDouble(1)
{
Default = 1,
MinValue = 0.5,
MaxValue = 2,
Precision = 0.1,
};
private readonly PlayerSliderBar<double> rateSlider; private readonly PlayerSliderBar<double> rateSlider;
@ -47,31 +52,13 @@ namespace osu.Game.Screens.Play.PlayerSettings
} }
}, },
}, },
rateSlider = new PlayerSliderBar<double> rateSlider = new PlayerSliderBar<double> { Bindable = UserPlaybackRate }
{
Bindable = new BindableDouble(1)
{
Default = 1,
MinValue = 0.5,
MaxValue = 2,
Precision = 0.1,
},
}
}; };
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
if (AdjustableClock == null)
return;
var clockRate = AdjustableClock.Rate;
// can't trigger this line instantly as the underlying clock may not be ready to accept adjustments yet.
rateSlider.Bindable.ValueChanged += multiplier => AdjustableClock.Rate = clockRate * multiplier.NewValue;
rateSlider.Bindable.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); rateSlider.Bindable.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true);
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Humanizer; using Humanizer;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -64,7 +65,7 @@ namespace osu.Game.Screens.Select
{ {
Ruleset.Value = CurrentItem.Value.Ruleset; Ruleset.Value = CurrentItem.Value.Ruleset;
Beatmap.Value = beatmaps.GetWorkingBeatmap(CurrentItem.Value.Beatmap); Beatmap.Value = beatmaps.GetWorkingBeatmap(CurrentItem.Value.Beatmap);
Beatmap.Value.Mods.Value = selectedMods.Value = CurrentItem.Value.RequiredMods; Beatmap.Value.Mods.Value = selectedMods.Value = CurrentItem.Value.RequiredMods ?? Enumerable.Empty<Mod>();
} }
Beatmap.Disabled = true; Beatmap.Disabled = true;