mirror of
https://github.com/osukey/osukey.git
synced 2025-08-05 07:33:55 +09:00
Merge branch 'master' of https://github.com/ppy/osu into justusft/mania-color-snap
This commit is contained in:
@ -22,7 +22,11 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
// Audio
|
||||
new CheckAudioPresence(),
|
||||
new CheckAudioQuality()
|
||||
new CheckAudioQuality(),
|
||||
|
||||
// Compose
|
||||
new CheckUnsnappedObjects(),
|
||||
new CheckConcurrentObjects()
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap)
|
||||
|
88
osu.Game/Rulesets/Edit/Checks/CheckConcurrentObjects.cs
Normal file
88
osu.Game/Rulesets/Edit/Checks/CheckConcurrentObjects.cs
Normal file
@ -0,0 +1,88 @@
|
||||
// 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.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
public class CheckConcurrentObjects : ICheck
|
||||
{
|
||||
// We guarantee that the objects are either treated as concurrent or unsnapped when near the same beat divisor.
|
||||
private const double ms_leniency = CheckUnsnappedObjects.UNSNAP_MS_THRESHOLD;
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Concurrent hitobjects");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateConcurrentSame(this),
|
||||
new IssueTemplateConcurrentDifferent(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
|
||||
{
|
||||
for (int i = 0; i < playableBeatmap.HitObjects.Count - 1; ++i)
|
||||
{
|
||||
var hitobject = playableBeatmap.HitObjects[i];
|
||||
|
||||
for (int j = i + 1; j < playableBeatmap.HitObjects.Count; ++j)
|
||||
{
|
||||
var nextHitobject = playableBeatmap.HitObjects[j];
|
||||
|
||||
// Accounts for rulesets with hitobjects separated by columns, such as Mania.
|
||||
// In these cases we only care about concurrent objects within the same column.
|
||||
if ((hitobject as IHasColumn)?.Column != (nextHitobject as IHasColumn)?.Column)
|
||||
continue;
|
||||
|
||||
// Two hitobjects cannot be concurrent without also being concurrent with all objects in between.
|
||||
// So if the next object is not concurrent, then we know no future objects will be either.
|
||||
if (!areConcurrent(hitobject, nextHitobject))
|
||||
break;
|
||||
|
||||
if (hitobject.GetType() == nextHitobject.GetType())
|
||||
yield return new IssueTemplateConcurrentSame(this).Create(hitobject, nextHitobject);
|
||||
else
|
||||
yield return new IssueTemplateConcurrentDifferent(this).Create(hitobject, nextHitobject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool areConcurrent(HitObject hitobject, HitObject nextHitobject) => nextHitobject.StartTime <= hitobject.GetEndTime() + ms_leniency;
|
||||
|
||||
public abstract class IssueTemplateConcurrent : IssueTemplate
|
||||
{
|
||||
protected IssueTemplateConcurrent(ICheck check, string unformattedMessage)
|
||||
: base(check, IssueType.Problem, unformattedMessage)
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(HitObject hitobject, HitObject nextHitobject)
|
||||
{
|
||||
var hitobjects = new List<HitObject> { hitobject, nextHitobject };
|
||||
return new Issue(hitobjects, this, hitobject.GetType().Name, nextHitobject.GetType().Name)
|
||||
{
|
||||
Time = nextHitobject.StartTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateConcurrentSame : IssueTemplateConcurrent
|
||||
{
|
||||
public IssueTemplateConcurrentSame(ICheck check)
|
||||
: base(check, "{0}s are concurrent here.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateConcurrentDifferent : IssueTemplateConcurrent
|
||||
{
|
||||
public IssueTemplateConcurrentDifferent(ICheck check)
|
||||
: base(check, "{0} and {1} are concurrent here.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
100
osu.Game/Rulesets/Edit/Checks/CheckUnsnappedObjects.cs
Normal file
100
osu.Game/Rulesets/Edit/Checks/CheckUnsnappedObjects.cs
Normal file
@ -0,0 +1,100 @@
|
||||
// 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;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
public class CheckUnsnappedObjects : ICheck
|
||||
{
|
||||
public const double UNSNAP_MS_THRESHOLD = 2;
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Timing, "Unsnapped hitobjects");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateLargeUnsnap(this),
|
||||
new IssueTemplateSmallUnsnap(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
|
||||
{
|
||||
var controlPointInfo = playableBeatmap.ControlPointInfo;
|
||||
|
||||
foreach (var hitobject in playableBeatmap.HitObjects)
|
||||
{
|
||||
double startUnsnap = hitobject.StartTime - controlPointInfo.GetClosestSnappedTime(hitobject.StartTime);
|
||||
string startPostfix = hitobject is IHasDuration ? "start" : "";
|
||||
foreach (var issue in getUnsnapIssues(hitobject, startUnsnap, hitobject.StartTime, startPostfix))
|
||||
yield return issue;
|
||||
|
||||
if (hitobject is IHasRepeats hasRepeats)
|
||||
{
|
||||
for (int repeatIndex = 0; repeatIndex < hasRepeats.RepeatCount; ++repeatIndex)
|
||||
{
|
||||
double spanDuration = hasRepeats.Duration / (hasRepeats.RepeatCount + 1);
|
||||
double repeatTime = hitobject.StartTime + spanDuration * (repeatIndex + 1);
|
||||
double repeatUnsnap = repeatTime - controlPointInfo.GetClosestSnappedTime(repeatTime);
|
||||
foreach (var issue in getUnsnapIssues(hitobject, repeatUnsnap, repeatTime, "repeat"))
|
||||
yield return issue;
|
||||
}
|
||||
}
|
||||
|
||||
if (hitobject is IHasDuration hasDuration)
|
||||
{
|
||||
double endUnsnap = hasDuration.EndTime - controlPointInfo.GetClosestSnappedTime(hasDuration.EndTime);
|
||||
foreach (var issue in getUnsnapIssues(hitobject, endUnsnap, hasDuration.EndTime, "end"))
|
||||
yield return issue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Issue> getUnsnapIssues(HitObject hitobject, double unsnap, double time, string postfix = "")
|
||||
{
|
||||
if (Math.Abs(unsnap) >= UNSNAP_MS_THRESHOLD)
|
||||
yield return new IssueTemplateLargeUnsnap(this).Create(hitobject, unsnap, time, postfix);
|
||||
else if (Math.Abs(unsnap) >= 1)
|
||||
yield return new IssueTemplateSmallUnsnap(this).Create(hitobject, unsnap, time, postfix);
|
||||
|
||||
// We don't care about unsnaps < 1 ms, as all object ends have these due to the way SV works.
|
||||
}
|
||||
|
||||
public abstract class IssueTemplateUnsnap : IssueTemplate
|
||||
{
|
||||
protected IssueTemplateUnsnap(ICheck check, IssueType type)
|
||||
: base(check, type, "{0} is unsnapped by {1:0.##} ms.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(HitObject hitobject, double unsnap, double time, string postfix = "")
|
||||
{
|
||||
string objectName = hitobject.GetType().Name;
|
||||
if (!string.IsNullOrEmpty(postfix))
|
||||
objectName += " " + postfix;
|
||||
|
||||
return new Issue(hitobject, this, objectName, unsnap) { Time = time };
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateLargeUnsnap : IssueTemplateUnsnap
|
||||
{
|
||||
public IssueTemplateLargeUnsnap(ICheck check)
|
||||
: base(check, IssueType.Problem)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateSmallUnsnap : IssueTemplateUnsnap
|
||||
{
|
||||
public IssueTemplateSmallUnsnap(ICheck check)
|
||||
: base(check, IssueType.Negligible)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// <summary>
|
||||
/// A wrapper for a <see cref="DrawableRuleset{TObject}"/>. Handles adding visual representations of <see cref="HitObject"/>s to the underlying <see cref="DrawableRuleset{TObject}"/>.
|
||||
/// </summary>
|
||||
internal class DrawableEditRulesetWrapper<TObject> : CompositeDrawable
|
||||
internal class DrawableEditorRulesetWrapper<TObject> : CompositeDrawable
|
||||
where TObject : HitObject
|
||||
{
|
||||
public Playfield Playfield => drawableRuleset.Playfield;
|
||||
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; }
|
||||
|
||||
public DrawableEditRulesetWrapper(DrawableRuleset<TObject> drawableRuleset)
|
||||
public DrawableEditorRulesetWrapper(DrawableRuleset<TObject> drawableRuleset)
|
||||
{
|
||||
this.drawableRuleset = drawableRuleset;
|
||||
|
@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
protected ComposeBlueprintContainer BlueprintContainer { get; private set; }
|
||||
|
||||
private DrawableEditRulesetWrapper<TObject> drawableRulesetWrapper;
|
||||
private DrawableEditorRulesetWrapper<TObject> drawableRulesetWrapper;
|
||||
|
||||
protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
try
|
||||
{
|
||||
drawableRulesetWrapper = new DrawableEditRulesetWrapper<TObject>(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap, new[] { Ruleset.GetAutoplayMod() }))
|
||||
drawableRulesetWrapper = new DrawableEditorRulesetWrapper<TObject>(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap, new[] { Ruleset.GetAutoplayMod() }))
|
||||
{
|
||||
Clock = EditorClock,
|
||||
ProcessCustomClock = false
|
||||
@ -182,8 +182,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// <summary>
|
||||
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
|
||||
/// </summary>
|
||||
protected virtual ComposeBlueprintContainer CreateBlueprintContainer()
|
||||
=> new ComposeBlueprintContainer(this);
|
||||
protected virtual ComposeBlueprintContainer CreateBlueprintContainer() => new ComposeBlueprintContainer(this);
|
||||
|
||||
/// <summary>
|
||||
/// Construct a drawable ruleset for the provided ruleset.
|
||||
|
@ -3,12 +3,13 @@
|
||||
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
public abstract class OverlaySelectionBlueprint : SelectionBlueprint
|
||||
public abstract class OverlaySelectionBlueprint : SelectionBlueprint<HitObject>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="DrawableHitObject"/> which this <see cref="OverlaySelectionBlueprint"/> applies to.
|
||||
@ -33,7 +34,5 @@ namespace osu.Game.Rulesets.Edit
|
||||
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre;
|
||||
|
||||
public override Quad SelectionQuad => DrawableObject.ScreenSpaceDrawQuad;
|
||||
|
||||
public override Vector2 GetInstantDelta(Vector2 screenSpacePosition) => DrawableObject.Parent.ToLocalSpace(screenSpacePosition) - DrawableObject.Position;
|
||||
}
|
||||
}
|
||||
|
@ -3,44 +3,38 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
/// <summary>
|
||||
/// A blueprint placed above a <see cref="DrawableHitObject"/> adding editing functionality.
|
||||
/// A blueprint placed above a displaying item adding editing functionality.
|
||||
/// </summary>
|
||||
public abstract class SelectionBlueprint : CompositeDrawable, IStateful<SelectionState>
|
||||
public abstract class SelectionBlueprint<T> : CompositeDrawable, IStateful<SelectionState>
|
||||
{
|
||||
public readonly HitObject HitObject;
|
||||
public readonly T Item;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="SelectionBlueprint"/> has been selected.
|
||||
/// Invoked when this <see cref="SelectionBlueprint{T}"/> has been selected.
|
||||
/// </summary>
|
||||
public event Action<SelectionBlueprint> Selected;
|
||||
public event Action<SelectionBlueprint<T>> Selected;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="SelectionBlueprint"/> has been deselected.
|
||||
/// Invoked when this <see cref="SelectionBlueprint{T}"/> has been deselected.
|
||||
/// </summary>
|
||||
public event Action<SelectionBlueprint> Deselected;
|
||||
public event Action<SelectionBlueprint<T>> Deselected;
|
||||
|
||||
public override bool HandlePositionalInput => ShouldBeAlive;
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private HitObjectComposer composer { get; set; }
|
||||
|
||||
protected SelectionBlueprint(HitObject hitObject)
|
||||
protected SelectionBlueprint(T item)
|
||||
{
|
||||
HitObject = hitObject;
|
||||
Item = item;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
AlwaysPresent = true;
|
||||
@ -91,7 +85,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
protected virtual void OnDeselected()
|
||||
{
|
||||
// selection blueprints are AlwaysPresent while the related DrawableHitObject is visible
|
||||
// selection blueprints are AlwaysPresent while the related item is visible
|
||||
// set the body piece's alpha directly to avoid arbitrarily rendering frame buffers etc. of children.
|
||||
foreach (var d in InternalChildren)
|
||||
d.Hide();
|
||||
@ -133,7 +127,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>();
|
||||
|
||||
/// <summary>
|
||||
/// The screen-space point that causes this <see cref="OverlaySelectionBlueprint"/> to be selected.
|
||||
/// The screen-space point that causes this <see cref="OverlaySelectionBlueprint"/> to be selected via a drag.
|
||||
/// </summary>
|
||||
public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre;
|
||||
|
||||
@ -142,8 +136,6 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// </summary>
|
||||
public virtual Quad SelectionQuad => ScreenSpaceDrawQuad;
|
||||
|
||||
public virtual Vector2 GetInstantDelta(Vector2 screenSpacePosition) => Parent.ToLocalSpace(screenSpacePosition) - Position;
|
||||
|
||||
/// <summary>
|
||||
/// Handle to perform a partial deletion when the user requests a quick delete (Shift+Right Click).
|
||||
/// </summary>
|
||||
|
57
osu.Game/Rulesets/Mods/ModBarrelRoll.cs
Normal file
57
osu.Game/Rulesets/Mods/ModBarrelRoll.cs
Normal file
@ -0,0 +1,57 @@
|
||||
// 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 osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModBarrelRoll<TObject> : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<TObject>
|
||||
where TObject : HitObject
|
||||
{
|
||||
/// <summary>
|
||||
/// The current angle of rotation being applied by this mod.
|
||||
/// Generally should be used to apply inverse rotation to elements which should not be rotated.
|
||||
/// </summary>
|
||||
protected float CurrentRotation { get; private set; }
|
||||
|
||||
[SettingSource("Roll speed", "Rotations per minute")]
|
||||
public BindableNumber<double> SpinSpeed { get; } = new BindableDouble(0.5)
|
||||
{
|
||||
MinValue = 0.02,
|
||||
MaxValue = 12,
|
||||
Precision = 0.01,
|
||||
};
|
||||
|
||||
[SettingSource("Direction", "The direction of rotation")]
|
||||
public Bindable<RotationDirection> Direction { get; } = new Bindable<RotationDirection>(RotationDirection.Clockwise);
|
||||
|
||||
public override string Name => "Barrel Roll";
|
||||
public override string Acronym => "BR";
|
||||
public override string Description => "The whole playfield is on a wheel!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override string SettingDescription => $"{SpinSpeed.Value} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
playfield.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
|
||||
}
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<TObject> drawableRuleset)
|
||||
{
|
||||
// scale the playfield to allow all hitobjects to stay within the visible region.
|
||||
|
||||
var playfieldSize = drawableRuleset.Playfield.DrawSize;
|
||||
var minSide = MathF.Min(playfieldSize.X, playfieldSize.Y);
|
||||
var maxSide = MathF.Max(playfieldSize.X, playfieldSize.Y);
|
||||
drawableRuleset.Playfield.Scale = new Vector2(minSide / maxSide);
|
||||
}
|
||||
}
|
||||
}
|
@ -11,21 +11,23 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Pooling;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Drawables
|
||||
{
|
||||
[Cached(typeof(DrawableHitObject))]
|
||||
public abstract class DrawableHitObject : SkinReloadableDrawable
|
||||
public abstract class DrawableHitObject : PoolableDrawableWithLifetime<HitObjectLifetimeEntry>
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked after this <see cref="DrawableHitObject"/>'s applied <see cref="HitObject"/> has had its defaults applied.
|
||||
@ -40,7 +42,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// <summary>
|
||||
/// The <see cref="HitObject"/> currently represented by this <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
public HitObject HitObject => lifetimeEntry?.HitObject;
|
||||
public HitObject HitObject => Entry?.HitObject;
|
||||
|
||||
/// <summary>
|
||||
/// The parenting <see cref="DrawableHitObject"/>, if any.
|
||||
@ -109,7 +111,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// <summary>
|
||||
/// The scoring result of this <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
public JudgementResult Result => lifetimeEntry?.Result;
|
||||
public JudgementResult Result => Entry?.Result;
|
||||
|
||||
/// <summary>
|
||||
/// The relative X position of this hit object for sample playback balance adjustment.
|
||||
@ -125,8 +127,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
private readonly Bindable<bool> userPositionalHitSounds = new Bindable<bool>();
|
||||
private readonly Bindable<int> comboIndexBindable = new Bindable<int>();
|
||||
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
public override bool RemoveCompletedTransforms => false;
|
||||
protected override bool RequiresChildrenUpdate => true;
|
||||
|
||||
public override bool IsPresent => base.IsPresent || (State.Value == ArmedState.Idle && Clock?.CurrentTime >= LifetimeStart);
|
||||
@ -141,18 +141,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// </remarks>
|
||||
public IBindable<ArmedState> State => state;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitObjectLifetimeEntry"/> is currently applied.
|
||||
/// </summary>
|
||||
private bool hasEntryApplied;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of the currently-attached <see cref="HitObject"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Even if it is not null, it may not be fully applied until loaded (<see cref="hasEntryApplied"/> is false).</remarks>
|
||||
[CanBeNull]
|
||||
private HitObjectLifetimeEntry lifetimeEntry;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IPooledHitObjectProvider pooledObjectProvider { get; set; }
|
||||
|
||||
@ -166,32 +154,25 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// </summary>
|
||||
/// <param name="initialHitObject">
|
||||
/// The <see cref="HitObject"/> to be initially applied to this <see cref="DrawableHitObject"/>.
|
||||
/// If <c>null</c>, a hitobject is expected to be later applied via <see cref="Apply(osu.Game.Rulesets.Objects.HitObjectLifetimeEntry)"/> (or automatically via pooling).
|
||||
/// If <c>null</c>, a hitobject is expected to be later applied via <see cref="PoolableDrawableWithLifetime{TEntry}.Apply"/> (or automatically via pooling).
|
||||
/// </param>
|
||||
protected DrawableHitObject([CanBeNull] HitObject initialHitObject = null)
|
||||
: base(initialHitObject != null ? new SyntheticHitObjectEntry(initialHitObject) : null)
|
||||
{
|
||||
if (initialHitObject != null)
|
||||
{
|
||||
lifetimeEntry = new SyntheticHitObjectEntry(initialHitObject);
|
||||
if (Entry != null)
|
||||
ensureEntryHasResult();
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
private void load(OsuConfigManager config, ISkinSource skinSource)
|
||||
{
|
||||
config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds);
|
||||
|
||||
// Explicit non-virtual function call.
|
||||
base.AddInternal(Samples = new PausableSkinnableSound());
|
||||
}
|
||||
|
||||
protected override void LoadAsyncComplete()
|
||||
{
|
||||
base.LoadAsyncComplete();
|
||||
|
||||
if (lifetimeEntry != null && !hasEntryApplied)
|
||||
Apply(lifetimeEntry);
|
||||
CurrentSkin = skinSource;
|
||||
CurrentSkin.SourceChanged += onSkinSourceChanged;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -227,22 +208,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
Apply(new SyntheticHitObjectEntry(hitObject));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a new <see cref="HitObjectLifetimeEntry"/> to be represented by this <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
public void Apply([NotNull] HitObjectLifetimeEntry newEntry)
|
||||
protected sealed override void OnApply(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
free();
|
||||
|
||||
lifetimeEntry = newEntry;
|
||||
|
||||
// LifetimeStart is already computed using HitObjectLifetimeEntry's InitialLifetimeOffset.
|
||||
// We override this with DHO's InitialLifetimeOffset for a non-pooled DHO.
|
||||
if (newEntry is SyntheticHitObjectEntry)
|
||||
lifetimeEntry.LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
|
||||
|
||||
LifetimeStart = lifetimeEntry.LifetimeStart;
|
||||
LifetimeEnd = lifetimeEntry.LifetimeEnd;
|
||||
if (entry is SyntheticHitObjectEntry)
|
||||
LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
|
||||
|
||||
ensureEntryHasResult();
|
||||
|
||||
@ -293,17 +264,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
else
|
||||
updateState(ArmedState.Idle, true);
|
||||
}
|
||||
|
||||
hasEntryApplied = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the currently applied <see cref="lifetimeEntry"/>
|
||||
/// </summary>
|
||||
private void free()
|
||||
protected sealed override void OnFree(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
if (!hasEntryApplied) return;
|
||||
|
||||
StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
|
||||
if (HitObject is IHasComboInformation combo)
|
||||
comboIndexBindable.UnbindFrom(combo.ComboIndexBindable);
|
||||
@ -335,22 +299,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
OnFree();
|
||||
|
||||
ParentHitObject = null;
|
||||
lifetimeEntry = null;
|
||||
|
||||
clearExistingStateTransforms();
|
||||
|
||||
hasEntryApplied = false;
|
||||
}
|
||||
|
||||
protected sealed override void FreeAfterUse()
|
||||
{
|
||||
base.FreeAfterUse();
|
||||
|
||||
// Freeing while not in a pool would cause the DHO to not be usable elsewhere in the hierarchy without being re-applied.
|
||||
if (!IsInPool)
|
||||
return;
|
||||
|
||||
free();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -398,8 +348,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
private void onDefaultsApplied(HitObject hitObject)
|
||||
{
|
||||
Debug.Assert(lifetimeEntry != null);
|
||||
Apply(lifetimeEntry);
|
||||
Debug.Assert(Entry != null);
|
||||
Apply(Entry);
|
||||
|
||||
DefaultsApplied?.Invoke(this);
|
||||
}
|
||||
@ -480,9 +430,14 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
base.ClearTransformsAfter(double.MinValue, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reapplies the current <see cref="ArmedState"/>.
|
||||
/// </summary>
|
||||
protected void RefreshStateTransforms() => updateState(State.Value, true);
|
||||
|
||||
/// <summary>
|
||||
/// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time.
|
||||
/// The local drawable hierarchy is recursively delayed to <see cref="LifetimeStart"/> for convenience.
|
||||
/// The local drawable hierarchy is recursively delayed to <see cref="LifetimeEntry.LifetimeStart"/> for convenience.
|
||||
///
|
||||
/// By default this will fade in the object from zero with no duration.
|
||||
/// </summary>
|
||||
@ -536,17 +491,19 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
#endregion
|
||||
|
||||
protected sealed override void SkinChanged(ISkinSource skin, bool allowFallback)
|
||||
{
|
||||
base.SkinChanged(skin, allowFallback);
|
||||
#region Skinning
|
||||
|
||||
protected ISkinSource CurrentSkin { get; private set; }
|
||||
|
||||
private void onSkinSourceChanged() => Scheduler.AddOnce(() =>
|
||||
{
|
||||
UpdateComboColour();
|
||||
|
||||
ApplySkin(skin, allowFallback);
|
||||
ApplySkin(CurrentSkin, true);
|
||||
|
||||
if (IsLoaded)
|
||||
updateState(State.Value, true);
|
||||
}
|
||||
});
|
||||
|
||||
protected void UpdateComboColour()
|
||||
{
|
||||
@ -616,6 +573,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
Samples.Stop();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@ -653,30 +612,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// </remarks>
|
||||
protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action);
|
||||
|
||||
public override double LifetimeStart
|
||||
{
|
||||
get => base.LifetimeStart;
|
||||
set => setLifetime(value, LifetimeEnd);
|
||||
}
|
||||
|
||||
public override double LifetimeEnd
|
||||
{
|
||||
get => base.LifetimeEnd;
|
||||
set => setLifetime(LifetimeStart, value);
|
||||
}
|
||||
|
||||
private void setLifetime(double lifetimeStart, double lifetimeEnd)
|
||||
{
|
||||
base.LifetimeStart = lifetimeStart;
|
||||
base.LifetimeEnd = lifetimeEnd;
|
||||
|
||||
if (lifetimeEntry != null)
|
||||
{
|
||||
lifetimeEntry.LifetimeStart = lifetimeStart;
|
||||
lifetimeEntry.LifetimeEnd = lifetimeEnd;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A safe offset prior to the start time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> may begin displaying contents.
|
||||
/// By default, <see cref="DrawableHitObject"/>s are assumed to display their contents within 10 seconds prior to the start time of <see cref="HitObject"/>.
|
||||
@ -684,7 +619,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// <remarks>
|
||||
/// This is only used as an optimisation to delay the initial update of this <see cref="DrawableHitObject"/> and may be tuned more aggressively if required.
|
||||
/// It is indirectly used to decide the automatic transform offset provided to <see cref="UpdateInitialTransforms"/>.
|
||||
/// A more accurate <see cref="LifetimeStart"/> should be set for further optimisation (in <see cref="LoadComplete"/>, for example).
|
||||
/// A more accurate <see cref="LifetimeEntry.LifetimeStart"/> should be set for further optimisation (in <see cref="LoadComplete"/>, for example).
|
||||
/// <para>
|
||||
/// Only has an effect if this <see cref="DrawableHitObject"/> is not being pooled.
|
||||
/// For pooled <see cref="DrawableHitObject"/>s, use <see cref="HitObjectLifetimeEntry.InitialLifetimeOffset"/> instead.
|
||||
@ -800,9 +735,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
private void ensureEntryHasResult()
|
||||
{
|
||||
Debug.Assert(lifetimeEntry != null);
|
||||
lifetimeEntry.Result ??= CreateResult(HitObject.CreateJudgement())
|
||||
?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
|
||||
Debug.Assert(Entry != null);
|
||||
Entry.Result ??= CreateResult(HitObject.CreateJudgement())
|
||||
?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
@ -811,6 +746,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
if (HitObject != null)
|
||||
HitObject.DefaultsApplied -= onDefaultsApplied;
|
||||
|
||||
CurrentSkin.SourceChanged -= onSkinSourceChanged;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,40 +38,23 @@ namespace osu.Game.Rulesets.Objects
|
||||
startTimeBindable.BindValueChanged(onStartTimeChanged, true);
|
||||
}
|
||||
|
||||
// The lifetime start, as set by the hitobject.
|
||||
// The lifetime, as set by the hitobject.
|
||||
private double realLifetimeStart = double.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// The time at which the <see cref="HitObject"/> should become alive.
|
||||
/// </summary>
|
||||
public new double LifetimeStart
|
||||
{
|
||||
get => realLifetimeStart;
|
||||
set => setLifetime(realLifetimeStart = value, LifetimeEnd);
|
||||
}
|
||||
|
||||
// The lifetime end, as set by the hitobject.
|
||||
private double realLifetimeEnd = double.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// The time at which the <see cref="HitObject"/> should become dead.
|
||||
/// </summary>
|
||||
public new double LifetimeEnd
|
||||
// This method is called even if `start == LifetimeStart` when `KeepAlive` is true (necessary to update `realLifetimeStart`).
|
||||
protected override void SetLifetimeStart(double start)
|
||||
{
|
||||
get => realLifetimeEnd;
|
||||
set => setLifetime(LifetimeStart, realLifetimeEnd = value);
|
||||
realLifetimeStart = start;
|
||||
if (!keepAlive)
|
||||
base.SetLifetimeStart(start);
|
||||
}
|
||||
|
||||
private void setLifetime(double start, double end)
|
||||
protected override void SetLifetimeEnd(double end)
|
||||
{
|
||||
if (keepAlive)
|
||||
{
|
||||
start = double.MinValue;
|
||||
end = double.MaxValue;
|
||||
}
|
||||
|
||||
base.LifetimeStart = start;
|
||||
base.LifetimeEnd = end;
|
||||
realLifetimeEnd = end;
|
||||
if (!keepAlive)
|
||||
base.SetLifetimeEnd(end);
|
||||
}
|
||||
|
||||
private bool keepAlive;
|
||||
@ -87,7 +70,10 @@ namespace osu.Game.Rulesets.Objects
|
||||
return;
|
||||
|
||||
keepAlive = value;
|
||||
setLifetime(realLifetimeStart, realLifetimeEnd);
|
||||
if (keepAlive)
|
||||
SetLifetime(double.MinValue, double.MaxValue);
|
||||
else
|
||||
SetLifetime(realLifetimeStart, realLifetimeEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,12 +84,12 @@ namespace osu.Game.Rulesets.Objects
|
||||
/// <remarks>
|
||||
/// This is only used as an optimisation to delay the initial update of the <see cref="HitObject"/> and may be tuned more aggressively if required.
|
||||
/// It is indirectly used to decide the automatic transform offset provided to <see cref="DrawableHitObject.UpdateInitialTransforms"/>.
|
||||
/// A more accurate <see cref="LifetimeStart"/> should be set for further optimisation (in <see cref="DrawableHitObject.LoadComplete"/>, for example).
|
||||
/// A more accurate <see cref="LifetimeEntry.LifetimeStart"/> should be set for further optimisation (in <see cref="DrawableHitObject.LoadComplete"/>, for example).
|
||||
/// </remarks>
|
||||
protected virtual double InitialLifetimeOffset => 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Resets <see cref="LifetimeStart"/> according to the change in start time of the <see cref="HitObject"/>.
|
||||
/// Resets <see cref="LifetimeEntry.LifetimeStart"/> according to the change in start time of the <see cref="HitObject"/>.
|
||||
/// </summary>
|
||||
private void onStartTimeChanged(ValueChangedEvent<double> startTime) => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
|
||||
}
|
||||
|
@ -0,0 +1,122 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Pooling
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="PoolableDrawable"/> that is controlled by <see cref="Entry"/> to implement drawable pooling and replay rewinding.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntry">The <see cref="LifetimeEntry"/> type storing state and controlling this drawable.</typeparam>
|
||||
public abstract class PoolableDrawableWithLifetime<TEntry> : PoolableDrawable where TEntry : LifetimeEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// The entry holding essential state of this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
|
||||
/// </summary>
|
||||
protected TEntry? Entry { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="Entry"/> is applied to this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
|
||||
/// When an initial entry is specified in the constructor, <see cref="Entry"/> is set but not applied until loading is completed.
|
||||
/// </summary>
|
||||
protected bool HasEntryApplied { get; private set; }
|
||||
|
||||
public override double LifetimeStart
|
||||
{
|
||||
get => base.LifetimeStart;
|
||||
set => setLifetime(value, LifetimeEnd);
|
||||
}
|
||||
|
||||
public override double LifetimeEnd
|
||||
{
|
||||
get => base.LifetimeEnd;
|
||||
set => setLifetime(LifetimeStart, value);
|
||||
}
|
||||
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
public override bool RemoveCompletedTransforms => false;
|
||||
|
||||
protected PoolableDrawableWithLifetime(TEntry? initialEntry = null)
|
||||
{
|
||||
Entry = initialEntry;
|
||||
}
|
||||
|
||||
protected override void LoadAsyncComplete()
|
||||
{
|
||||
base.LoadAsyncComplete();
|
||||
|
||||
// Apply the initial entry given in the constructor.
|
||||
if (Entry != null && !HasEntryApplied)
|
||||
Apply(Entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a new entry to be represented by this drawable.
|
||||
/// If there is an existing entry applied, the entry will be replaced.
|
||||
/// </summary>
|
||||
public void Apply(TEntry entry)
|
||||
{
|
||||
if (HasEntryApplied)
|
||||
free();
|
||||
|
||||
setLifetime(entry.LifetimeStart, entry.LifetimeEnd);
|
||||
Entry = entry;
|
||||
|
||||
OnApply(entry);
|
||||
|
||||
HasEntryApplied = true;
|
||||
}
|
||||
|
||||
protected sealed override void FreeAfterUse()
|
||||
{
|
||||
base.FreeAfterUse();
|
||||
|
||||
// We preserve the existing entry in case we want to move a non-pooled drawable between different parent drawables.
|
||||
if (HasEntryApplied && IsInPool)
|
||||
free();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked to apply a new entry to this drawable.
|
||||
/// </summary>
|
||||
protected virtual void OnApply(TEntry entry)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked to revert application of the entry to this drawable.
|
||||
/// </summary>
|
||||
protected virtual void OnFree(TEntry entry)
|
||||
{
|
||||
}
|
||||
|
||||
private void setLifetime(double start, double end)
|
||||
{
|
||||
base.LifetimeStart = start;
|
||||
base.LifetimeEnd = end;
|
||||
|
||||
if (Entry != null)
|
||||
{
|
||||
Entry.LifetimeStart = start;
|
||||
Entry.LifetimeEnd = end;
|
||||
}
|
||||
}
|
||||
|
||||
private void free()
|
||||
{
|
||||
Debug.Assert(Entry != null && HasEntryApplied);
|
||||
|
||||
OnFree(Entry);
|
||||
|
||||
Entry = null;
|
||||
setLifetime(double.MaxValue, double.MaxValue);
|
||||
|
||||
HasEntryApplied = false;
|
||||
}
|
||||
}
|
||||
}
|
16
osu.Game/Rulesets/Objects/Types/IHasColumn.cs
Normal file
16
osu.Game/Rulesets/Objects/Types/IHasColumn.cs
Normal file
@ -0,0 +1,16 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// A type of hit object which lies in one of a number of predetermined columns.
|
||||
/// </summary>
|
||||
public interface IHasColumn
|
||||
{
|
||||
/// <summary>
|
||||
/// The column which the hit object lies in.
|
||||
/// </summary>
|
||||
int Column { get; }
|
||||
}
|
||||
}
|
@ -252,7 +252,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value;
|
||||
}
|
||||
|
||||
return GetScore(mode, maxAchievableCombo, calculateAccuracyRatio(computedBaseScore), calculateComboRatio(maxCombo), scoreResultCounts);
|
||||
return GetScore(mode, maxAchievableCombo, calculateAccuracyRatio(computedBaseScore), calculateComboRatio(maxCombo), statistics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -266,7 +266,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
if (preferRolling && rollingMaxBaseScore != 0)
|
||||
return baseScore / rollingMaxBaseScore;
|
||||
|
||||
return maxBaseScore > 0 ? baseScore / maxBaseScore : 0;
|
||||
return maxBaseScore > 0 ? baseScore / maxBaseScore : 1;
|
||||
}
|
||||
|
||||
private double calculateComboRatio(int maxCombo) => maxAchievableCombo > 0 ? (double)maxCombo / maxAchievableCombo : 1;
|
||||
|
Reference in New Issue
Block a user