1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 07:07:45 +08:00

Merge pull request #10717 from smoogipoo/dho-apply

This commit is contained in:
Dean Herbert 2020-11-11 10:54:24 +09:00 committed by GitHub
commit 8706b51b77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 358 additions and 79 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1109.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1110.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,44 @@
// 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 NUnit.Framework;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneHitCircleApplication : OsuTestScene
{
[Test]
public void TestApplyNewCircle()
{
DrawableHitCircle dho = null;
AddStep("create circle", () => Child = dho = new DrawableHitCircle(prepareObject(new HitCircle
{
Position = new Vector2(256, 192),
IndexInCurrentCombo = 0
}))
{
Clock = new FramedClock(new StopwatchClock())
});
AddStep("apply new circle", () => dho.Apply(prepareObject(new HitCircle
{
Position = new Vector2(128, 128),
ComboIndex = 1,
})));
}
private HitCircle prepareObject(HitCircle circle)
{
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return circle;
}
}
}

View File

@ -0,0 +1,59 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSliderApplication : OsuTestScene
{
[Test]
public void TestApplyNewSlider()
{
DrawableSlider dho = null;
AddStep("create slider", () => Child = dho = new DrawableSlider(prepareObject(new Slider
{
Position = new Vector2(256, 192),
IndexInCurrentCombo = 0,
StartTime = Time.Current,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(150, 100),
new Vector2(300, 0),
})
})));
AddWaitStep("wait for progression", 1);
AddStep("apply new slider", () => dho.Apply(prepareObject(new Slider
{
Position = new Vector2(256, 192),
ComboIndex = 1,
StartTime = dho.HitObject.StartTime,
Path = new SliderPath(PathType.Bezier, new[]
{
Vector2.Zero,
new Vector2(150, 100),
new Vector2(300, 0),
}),
RepeatCount = 1
})));
}
private Slider prepareObject(Slider slider)
{
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return slider;
}
}
}

View File

@ -0,0 +1,46 @@
// 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 NUnit.Framework;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSpinnerApplication : OsuTestScene
{
[Test]
public void TestApplyNewCircle()
{
DrawableSpinner dho = null;
AddStep("create spinner", () => Child = dho = new DrawableSpinner(prepareObject(new Spinner
{
Position = new Vector2(256, 192),
IndexInCurrentCombo = 0,
Duration = 0,
}))
{
Clock = new FramedClock(new StopwatchClock())
});
AddStep("apply new spinner", () => dho.Apply(prepareObject(new Spinner
{
Position = new Vector2(256, 192),
ComboIndex = 1,
Duration = 1000,
})));
}
private Spinner prepareObject(Spinner circle)
{
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return circle;
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Container scaleContainer;
private InputManager inputManager;
public DrawableHitCircle(HitCircle h)
public DrawableHitCircle([CanBeNull] HitCircle h = null)
: base(h)
{
}
@ -72,10 +73,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Size = HitArea.DrawSize;
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true);
StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true);
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true);
AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue, true);
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue);
}
protected override void LoadComplete()

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
@ -49,6 +50,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ShakeDuration = 30,
RelativeSizeAxes = Axes.Both
});
}
protected override void OnApply(HitObject hitObject)
{
base.OnApply(hitObject);
IndexInCurrentComboBindable.BindTo(HitObject.IndexInCurrentComboBindable);
PositionBindable.BindTo(HitObject.PositionBindable);
@ -56,6 +62,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ScaleBindable.BindTo(HitObject.ScaleBindable);
}
protected override void OnFree(HitObject hitObject)
{
base.OnFree(hitObject);
IndexInCurrentComboBindable.UnbindFrom(HitObject.IndexInCurrentComboBindable);
PositionBindable.UnbindFrom(HitObject.PositionBindable);
StackHeightBindable.UnbindFrom(HitObject.StackHeightBindable);
ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
}
// Forward all internal management to shakeContainer.
// This is a bit ugly but we don't have the concept of InternalContent so it'll have to do for now. (https://github.com/ppy/osu-framework/issues/1690)
protected override void AddInternal(Drawable drawable) => shakeContainer.Add(drawable);

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using osuTK;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
@ -32,14 +33,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody;
public readonly IBindable<int> PathVersion = new Bindable<int>();
public IBindable<int> PathVersion => pathVersion;
private readonly Bindable<int> pathVersion = new Bindable<int>();
private Container<DrawableSliderHead> headContainer;
private Container<DrawableSliderTail> tailContainer;
private Container<DrawableSliderTick> tickContainer;
private Container<DrawableSliderRepeat> repeatContainer;
public DrawableSlider(Slider s)
public DrawableSlider([CanBeNull] Slider s = null)
: base(s)
{
}
@ -63,11 +65,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
};
PathVersion.BindTo(HitObject.Path.Version);
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true);
StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true);
ScaleBindable.BindValueChanged(scale => Ball.Scale = new Vector2(scale.NewValue), true);
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
ScaleBindable.BindValueChanged(scale => Ball.Scale = new Vector2(scale.NewValue));
AccentColour.BindValueChanged(colour =>
{
@ -78,6 +78,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.BindValueChanged(updateSlidingSample);
}
protected override void OnApply(HitObject hitObject)
{
base.OnApply(hitObject);
// Ensure that the version will change after the upcoming BindTo().
pathVersion.Value = int.MaxValue;
PathVersion.BindTo(HitObject.Path.Version);
}
protected override void OnFree(HitObject hitObject)
{
base.OnFree(hitObject);
PathVersion.UnbindFrom(HitObject.Path.Version);
}
private PausableSkinnableSound slidingSample;
protected override void LoadSamples()

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Bindable<bool> isSpinning;
private bool spinnerFrequencyModulate;
public DrawableSpinner(Spinner s)
public DrawableSpinner([CanBeNull] Spinner s = null)
: base(s)
{
}
@ -72,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
};
PositionBindable.BindValueChanged(pos => Position = pos.NewValue, true);
PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
}
protected override void LoadComplete()

View File

@ -1,7 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Scoring;
@ -28,9 +27,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
// suppress locally to allow hiding the visuals wherever necessary.
}
[BackgroundDependencyLoader]
private void load()
protected override void LoadComplete()
{
base.LoadComplete();
Result.Type = Type;
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@ -27,7 +28,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
{
public event Action<DrawableHitObject> DefaultsApplied;
public readonly HitObject HitObject;
/// <summary>
/// The <see cref="HitObject"/> currently represented by this <see cref="DrawableHitObject"/>.
/// </summary>
public HitObject HitObject { get; private set; }
/// <summary>
/// The colour used for various elements of this DrawableHitObject.
@ -96,10 +100,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </remarks>
protected virtual float SamplePlaybackPosition => 0.5f;
private BindableList<HitSampleInfo> samplesBindable;
private Bindable<double> startTimeBindable;
private Bindable<bool> userPositionalHitSounds;
private Bindable<int> comboIndexBindable;
private readonly Bindable<double> startTimeBindable = new Bindable<double>();
private readonly BindableList<HitSampleInfo> samplesBindable = new BindableList<HitSampleInfo>();
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;
@ -111,52 +115,157 @@ namespace osu.Game.Rulesets.Objects.Drawables
public IBindable<ArmedState> State => state;
protected DrawableHitObject([NotNull] HitObject hitObject)
/// <summary>
/// Whether <see cref="HitObject"/> is currently applied.
/// </summary>
private bool hasHitObjectApplied;
/// <summary>
/// Creates a new <see cref="DrawableHitObject"/>.
/// </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"/> (or automatically via pooling).
/// </param>
protected DrawableHitObject([CanBeNull] HitObject initialHitObject = null)
{
HitObject = hitObject ?? throw new ArgumentNullException(nameof(hitObject));
HitObject = initialHitObject;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
userPositionalHitSounds = config.GetBindable<bool>(OsuSetting.PositionalHitSounds);
var judgement = HitObject.CreateJudgement();
Result = CreateResult(judgement);
if (Result == null)
throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds);
}
protected override void LoadAsyncComplete()
{
base.LoadAsyncComplete();
LoadSamples();
HitObject.DefaultsApplied += onDefaultsApplied;
startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
startTimeBindable.BindValueChanged(_ => updateState(State.Value, true));
if (HitObject is IHasComboInformation combo)
{
comboIndexBindable = combo.ComboIndexBindable.GetBoundCopy();
comboIndexBindable.BindValueChanged(_ => updateComboColour(), true);
}
samplesBindable = HitObject.SamplesBindable.GetBoundCopy();
samplesBindable.CollectionChanged += (_, __) => LoadSamples();
apply(HitObject);
if (HitObject != null)
Apply(HitObject);
}
protected override void LoadComplete()
{
base.LoadComplete();
startTimeBindable.BindValueChanged(_ => updateState(State.Value, true));
comboIndexBindable.BindValueChanged(_ => updateComboColour(), true);
updateState(ArmedState.Idle, true);
}
/// <summary>
/// Applies a new <see cref="HitObject"/> to be represented by this <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to apply.</param>
public void Apply(HitObject hitObject)
{
free();
HitObject = hitObject ?? throw new InvalidOperationException($"Cannot apply a null {nameof(HitObject)}.");
// Ensure this DHO has a result.
Result ??= CreateResult(HitObject.CreateJudgement())
?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
foreach (var h in HitObject.NestedHitObjects)
{
var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");
drawableNested.OnNewResult += onNewResult;
drawableNested.OnRevertResult += onRevertResult;
drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;
nestedHitObjects.Value.Add(drawableNested);
AddNestedHitObject(drawableNested);
}
startTimeBindable.BindTo(HitObject.StartTimeBindable);
if (HitObject is IHasComboInformation combo)
comboIndexBindable.BindTo(combo.ComboIndexBindable);
samplesBindable.BindTo(HitObject.SamplesBindable);
samplesBindable.BindCollectionChanged(onSamplesChanged, true);
HitObject.DefaultsApplied += onDefaultsApplied;
OnApply(hitObject);
// If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates.
if (IsLoaded)
Schedule(() => updateState(ArmedState.Idle, true));
hasHitObjectApplied = true;
}
/// <summary>
/// Removes the currently applied <see cref="HitObject"/>
/// </summary>
private void free()
{
if (!hasHitObjectApplied)
return;
startTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
if (HitObject is IHasComboInformation combo)
comboIndexBindable.UnbindFrom(combo.ComboIndexBindable);
samplesBindable.UnbindFrom(HitObject.SamplesBindable);
// When a new hitobject is applied, the samples will be cleared before re-populating.
// In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply().
samplesBindable.CollectionChanged -= onSamplesChanged;
if (nestedHitObjects.IsValueCreated)
{
foreach (var obj in nestedHitObjects.Value)
{
obj.OnNewResult -= onNewResult;
obj.OnRevertResult -= onRevertResult;
obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
nestedHitObjects.Value.Clear();
ClearNestedHitObjects();
}
HitObject.DefaultsApplied -= onDefaultsApplied;
OnFree(HitObject);
HitObject = null;
hasHitObjectApplied = 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>
/// Invoked for this <see cref="DrawableHitObject"/> to take on any values from a newly-applied <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> being applied.</param>
protected virtual void OnApply(HitObject hitObject)
{
}
/// <summary>
/// Invoked for this <see cref="DrawableHitObject"/> to revert any values previously taken on from the currently-applied <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The currently-applied <see cref="HitObject"/>.</param>
protected virtual void OnFree(HitObject hitObject)
{
}
/// <summary>
/// Invoked by the base <see cref="DrawableHitObject"/> to populate samples, once on initial load and potentially again on any change to the samples collection.
/// </summary>
@ -183,34 +292,20 @@ namespace osu.Game.Rulesets.Objects.Drawables
AddInternal(Samples);
}
private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples();
private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result);
private void onRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.Invoke(drawableHitObject, result);
private void onApplyCustomUpdateState(DrawableHitObject drawableHitObject, ArmedState state) => ApplyCustomUpdateState?.Invoke(drawableHitObject, state);
private void onDefaultsApplied(HitObject hitObject)
{
apply(hitObject);
updateState(state.Value, true);
Apply(hitObject);
DefaultsApplied?.Invoke(this);
}
private void apply(HitObject hitObject)
{
if (nestedHitObjects.IsValueCreated)
{
nestedHitObjects.Value.Clear();
ClearNestedHitObjects();
}
foreach (var h in hitObject.NestedHitObjects)
{
var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");
drawableNested.OnNewResult += (d, r) => OnNewResult?.Invoke(d, r);
drawableNested.OnRevertResult += (d, r) => OnRevertResult?.Invoke(d, r);
drawableNested.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j);
nestedHitObjects.Value.Add(drawableNested);
AddNestedHitObject(drawableNested);
}
}
/// <summary>
/// Invoked by the base <see cref="DrawableHitObject"/> to add nested <see cref="DrawableHitObject"/>s to the hierarchy.
/// </summary>
@ -600,19 +695,20 @@ namespace osu.Game.Rulesets.Objects.Drawables
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
HitObject.DefaultsApplied -= onDefaultsApplied;
if (HitObject != null)
HitObject.DefaultsApplied -= onDefaultsApplied;
}
}
public abstract class DrawableHitObject<TObject> : DrawableHitObject
where TObject : HitObject
{
public new readonly TObject HitObject;
public new TObject HitObject => (TObject)base.HitObject;
protected DrawableHitObject(TObject hitObject)
: base(hitObject)
{
HitObject = hitObject;
}
}
}

View File

@ -3,14 +3,14 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
namespace osu.Game.Skinning
{
/// <summary>
/// A drawable which has a callback when the skin changes.
/// </summary>
public abstract class SkinReloadableDrawable : CompositeDrawable
public abstract class SkinReloadableDrawable : PoolableDrawable
{
/// <summary>
/// Invoked when <see cref="CurrentSkin"/> has changed.

View File

@ -26,7 +26,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1109.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1110.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
<PackageReference Include="Sentry" Version="2.1.6" />
<PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1109.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1110.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -88,7 +88,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1109.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1110.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />