Merge branch 'master' into realm-key-binding-store

This commit is contained in:
Dean Herbert
2021-06-10 13:58:08 +09:00
340 changed files with 5939 additions and 2341 deletions

View File

@ -68,10 +68,7 @@ namespace osu.Game.Rulesets.UI
private bool frameStablePlayback = true;
/// <summary>
/// Whether to enable frame-stable playback.
/// </summary>
internal bool FrameStablePlayback
internal override bool FrameStablePlayback
{
get => frameStablePlayback;
set
@ -182,18 +179,11 @@ namespace osu.Game.Rulesets.UI
.WithChild(ResumeOverlay)));
}
RegenerateAutoplay();
applyRulesetMods(Mods, config);
loadObjects(cancellationToken ?? default);
}
public void RegenerateAutoplay()
{
// for now this is applying mods which aren't just autoplay.
// we'll need to reconsider this flow in the future.
applyRulesetMods(Mods, config);
}
/// <summary>
/// Creates and adds drawable representations of hit objects to the play field.
/// </summary>
@ -274,6 +264,12 @@ namespace osu.Game.Rulesets.UI
if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager))
throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports recording is not available");
if (score == null)
{
recordingInputManager.Recorder = null;
return;
}
var recorder = CreateReplayRecorder(score);
if (recorder == null)
@ -432,6 +428,11 @@ namespace osu.Game.Rulesets.UI
/// </summary>
public abstract IFrameStableClock FrameStableClock { get; }
/// <summary>
/// Whether to enable frame-stable playback.
/// </summary>
internal abstract bool FrameStablePlayback { get; set; }
/// <summary>
/// The mods which are to be applied.
/// </summary>
@ -518,7 +519,7 @@ namespace osu.Game.Rulesets.UI
/// Sets a replay to be used to record gameplay.
/// </summary>
/// <param name="score">The target to be recorded to.</param>
public abstract void SetRecordTarget(Score score);
public abstract void SetRecordTarget([CanBeNull] Score score);
/// <summary>
/// Invoked when the interactive user requests resuming from a paused state.

View File

@ -3,35 +3,23 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Pooling;
namespace osu.Game.Rulesets.UI
{
public class HitObjectContainer : CompositeDrawable, IHitObjectContainer
public class HitObjectContainer : PooledDrawableWithLifetimeContainer<HitObjectLifetimeEntry, DrawableHitObject>, IHitObjectContainer
{
/// <summary>
/// All entries in this <see cref="HitObjectContainer"/> including dead entries.
/// </summary>
public IEnumerable<HitObjectLifetimeEntry> Entries => allEntries;
/// <summary>
/// All alive entries and <see cref="DrawableHitObject"/>s used by the entries.
/// </summary>
public IEnumerable<(HitObjectLifetimeEntry Entry, DrawableHitObject Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value));
public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
public IEnumerable<DrawableHitObject> AliveObjects => AliveInternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
public IEnumerable<DrawableHitObject> AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime);
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is judged.
@ -59,34 +47,16 @@ namespace osu.Game.Rulesets.UI
/// </remarks>
internal event Action<HitObject> HitObjectUsageFinished;
/// <summary>
/// The amount of time prior to the current time within which <see cref="HitObject"/>s should be considered alive.
/// </summary>
internal double PastLifetimeExtension { get; set; }
/// <summary>
/// The amount of time after the current time within which <see cref="HitObject"/>s should be considered alive.
/// </summary>
internal double FutureLifetimeExtension { get; set; }
private readonly Dictionary<DrawableHitObject, IBindable> startTimeMap = new Dictionary<DrawableHitObject, IBindable>();
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> aliveDrawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> nonPooledDrawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
private readonly HashSet<HitObjectLifetimeEntry> allEntries = new HashSet<HitObjectLifetimeEntry>();
[Resolved(CanBeNull = true)]
private IPooledHitObjectProvider pooledObjectProvider { get; set; }
public HitObjectContainer()
{
RelativeSizeAxes = Axes.Both;
lifetimeManager.EntryBecameAlive += entryBecameAlive;
lifetimeManager.EntryBecameDead += entryBecameDead;
lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary;
}
protected override void LoadAsyncComplete()
@ -99,59 +69,40 @@ namespace osu.Game.Rulesets.UI
#region Pooling support
public void Add(HitObjectLifetimeEntry entry)
public override bool Remove(HitObjectLifetimeEntry entry)
{
allEntries.Add(entry);
lifetimeManager.AddEntry(entry);
}
public bool Remove(HitObjectLifetimeEntry entry)
{
if (!lifetimeManager.RemoveEntry(entry)) return false;
if (!base.Remove(entry)) return false;
// This logic is not in `Remove(DrawableHitObject)` because a non-pooled drawable may be removed by specifying its entry.
if (nonPooledDrawableMap.Remove(entry, out var drawable))
removeDrawable(drawable);
allEntries.Remove(entry);
return true;
}
private void entryBecameAlive(LifetimeEntry lifetimeEntry)
protected sealed override DrawableHitObject GetDrawable(HitObjectLifetimeEntry entry)
{
var entry = (HitObjectLifetimeEntry)lifetimeEntry;
Debug.Assert(!aliveDrawableMap.ContainsKey(entry));
if (nonPooledDrawableMap.TryGetValue(entry, out var drawable))
return drawable;
bool isNonPooled = nonPooledDrawableMap.TryGetValue(entry, out var drawable);
drawable ??= pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null);
if (drawable == null)
throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}.");
return pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null) ??
throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}.");
}
aliveDrawableMap[entry] = drawable;
OnAdd(drawable);
if (isNonPooled) return;
protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
{
if (nonPooledDrawableMap.ContainsKey(entry)) return;
addDrawable(drawable);
HitObjectUsageBegan?.Invoke(entry.HitObject);
}
private void entryBecameDead(LifetimeEntry lifetimeEntry)
protected override void RemoveDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
{
var entry = (HitObjectLifetimeEntry)lifetimeEntry;
Debug.Assert(aliveDrawableMap.ContainsKey(entry));
var drawable = aliveDrawableMap[entry];
bool isNonPooled = nonPooledDrawableMap.ContainsKey(entry);
drawable.OnKilled();
aliveDrawableMap.Remove(entry);
OnRemove(drawable);
if (isNonPooled) return;
if (nonPooledDrawableMap.ContainsKey(entry)) return;
removeDrawable(drawable);
// The hit object is not freed when the DHO was not pooled.
HitObjectUsageFinished?.Invoke(entry.HitObject);
}
@ -198,54 +149,8 @@ namespace osu.Game.Rulesets.UI
public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject);
private void entryCrossedBoundary(LifetimeEntry entry, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction)
{
if (nonPooledDrawableMap.TryGetValue((HitObjectLifetimeEntry)entry, out var drawable))
OnChildLifetimeBoundaryCrossed(new LifetimeBoundaryCrossedEvent(drawable, kind, direction));
}
protected virtual void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
{
}
#endregion
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is added to this container.
/// </summary>
/// <remarks>
/// This method is not invoked for nested <see cref="DrawableHitObject"/>s.
/// </remarks>
protected virtual void OnAdd(DrawableHitObject drawableHitObject)
{
}
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is removed from this container.
/// </summary>
/// <remarks>
/// This method is not invoked for nested <see cref="DrawableHitObject"/>s.
/// </remarks>
protected virtual void OnRemove(DrawableHitObject drawableHitObject)
{
}
public virtual void Clear()
{
lifetimeManager.ClearEntries();
foreach (var drawable in nonPooledDrawableMap.Values)
removeDrawable(drawable);
nonPooledDrawableMap.Clear();
Debug.Assert(InternalChildren.Count == 0 && startTimeMap.Count == 0 && aliveDrawableMap.Count == 0, "All hit objects should have been removed");
}
protected override bool CheckChildrenLife()
{
bool aliveChanged = base.CheckChildrenLife();
aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension);
return aliveChanged;
}
private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r);
private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r);

View File

@ -30,12 +30,14 @@ namespace osu.Game.Rulesets.UI
{
set
{
if (recorder != null)
if (value != null && recorder != null)
throw new InvalidOperationException("Cannot attach more than one recorder");
recorder?.Expire();
recorder = value;
KeyBindingContainer.Add(recorder);
if (recorder != null)
KeyBindingContainer.Add(recorder);
}
}

View File

@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Layout;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
@ -18,12 +20,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
/// <summary>
/// Hit objects which require lifetime computation in the next update call.
/// </summary>
private readonly HashSet<DrawableHitObject> toComputeLifetime = new HashSet<DrawableHitObject>();
/// <summary>
/// A set containing all <see cref="HitObjectContainer.AliveObjects"/> which have an up-to-date layout.
/// A set of top-level <see cref="DrawableHitObject"/>s which have an up-to-date layout.
/// </summary>
private readonly HashSet<DrawableHitObject> layoutComputed = new HashSet<DrawableHitObject>();
@ -50,14 +47,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
timeRange.ValueChanged += _ => layoutCache.Invalidate();
}
public override void Clear()
{
base.Clear();
toComputeLifetime.Clear();
layoutComputed.Clear();
}
/// <summary>
/// Given a position in screen space, return the time within this column.
/// </summary>
@ -83,7 +72,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
flipPositionIfRequired(ref position);
return scrollingInfo.Algorithm.TimeAt(position, Time.Current, scrollingInfo.TimeRange.Value, getLength());
return scrollingInfo.Algorithm.TimeAt(position, Time.Current, scrollingInfo.TimeRange.Value, scrollLength);
}
/// <summary>
@ -91,7 +80,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// </summary>
public Vector2 ScreenSpacePositionAtTime(double time)
{
var pos = scrollingInfo.Algorithm.PositionAt(time, Time.Current, scrollingInfo.TimeRange.Value, getLength());
var pos = scrollingInfo.Algorithm.PositionAt(time, Time.Current, scrollingInfo.TimeRange.Value, scrollLength);
flipPositionIfRequired(ref pos);
@ -106,16 +95,19 @@ namespace osu.Game.Rulesets.UI.Scrolling
}
}
private float getLength()
private float scrollLength
{
switch (scrollingInfo.Direction.Value)
get
{
case ScrollingDirection.Left:
case ScrollingDirection.Right:
return DrawWidth;
switch (scrollingInfo.Direction.Value)
{
case ScrollingDirection.Left:
case ScrollingDirection.Right:
return DrawWidth;
default:
return DrawHeight;
default:
return DrawHeight;
}
}
}
@ -150,81 +142,44 @@ namespace osu.Game.Rulesets.UI.Scrolling
}
}
protected override void OnAdd(DrawableHitObject drawableHitObject) => onAddRecursive(drawableHitObject);
protected override void OnRemove(DrawableHitObject drawableHitObject) => onRemoveRecursive(drawableHitObject);
private void onAddRecursive(DrawableHitObject hitObject)
protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
{
invalidateHitObject(hitObject);
base.AddDrawable(entry, drawable);
hitObject.DefaultsApplied += invalidateHitObject;
foreach (var nested in hitObject.NestedHitObjects)
onAddRecursive(nested);
invalidateHitObject(drawable);
drawable.DefaultsApplied += invalidateHitObject;
}
private void onRemoveRecursive(DrawableHitObject hitObject)
protected override void RemoveDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
{
toComputeLifetime.Remove(hitObject);
layoutComputed.Remove(hitObject);
base.RemoveDrawable(entry, drawable);
hitObject.DefaultsApplied -= invalidateHitObject;
foreach (var nested in hitObject.NestedHitObjects)
onRemoveRecursive(nested);
drawable.DefaultsApplied -= invalidateHitObject;
layoutComputed.Remove(drawable);
}
/// <summary>
/// Make this <see cref="DrawableHitObject"/> lifetime and layout computed in next update.
/// </summary>
private void invalidateHitObject(DrawableHitObject hitObject)
{
// Lifetime computation is delayed until next update because
// when the hit object is not pooled this container is not loaded here and `scrollLength` cannot be computed.
toComputeLifetime.Add(hitObject);
hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
layoutComputed.Remove(hitObject);
}
private float scrollLength;
protected override void Update()
{
base.Update();
if (!layoutCache.IsValid)
{
toComputeLifetime.Clear();
if (layoutCache.IsValid) return;
foreach (var hitObject in Objects)
{
if (hitObject.HitObject != null)
toComputeLifetime.Add(hitObject);
}
layoutComputed.Clear();
layoutComputed.Clear();
// Reset lifetime to the conservative estimation.
// If a drawable becomes alive by this lifetime, its lifetime will be updated to a more precise lifetime in the next update.
foreach (var entry in Entries)
entry.SetInitialLifetime();
scrollingInfo.Algorithm.Reset();
scrollingInfo.Algorithm.Reset();
switch (direction.Value)
{
case ScrollingDirection.Up:
case ScrollingDirection.Down:
scrollLength = DrawSize.Y;
break;
default:
scrollLength = DrawSize.X;
break;
}
layoutCache.Validate();
}
foreach (var hitObject in toComputeLifetime)
hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
toComputeLifetime.Clear();
layoutCache.Validate();
}
protected override void UpdateAfterChildrenLife()
@ -249,6 +204,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
{
// Origin position may be relative to the parent size
Debug.Assert(hitObject.Parent != null);
float originAdjustment = 0.0f;
// calculate the dimension of the part of the hitobject that should already be visible