mirror of
https://github.com/osukey/osukey.git
synced 2025-08-04 07:06:35 +09:00
Merge branch 'master' into realm-key-binding-store
This commit is contained in:
@ -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.
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user