1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-30 01:32:55 +08:00

Merge branch 'master' into editor-beat-snap-always

This commit is contained in:
Dean Herbert 2020-11-30 18:34:38 +09:00
commit c17d67bc7d
60 changed files with 981 additions and 474 deletions

View File

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

View File

@ -1,9 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osuTK; using osuTK;
@ -17,34 +18,49 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
base.LoadComplete(); base.LoadComplete();
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) AddStep("show pear", () => SetContents(() => createDrawableFruit(0)));
AddStep($"show {rep}", () => SetContents(() => createDrawableFruit(rep))); AddStep("show grape", () => SetContents(() => createDrawableFruit(1)));
AddStep("show pineapple / apple", () => SetContents(() => createDrawableFruit(2)));
AddStep("show raspberry / orange", () => SetContents(() => createDrawableFruit(3)));
AddStep("show banana", () => SetContents(createDrawableBanana));
AddStep("show droplet", () => SetContents(() => createDrawableDroplet())); AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet)); AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) AddStep("show hyperdash pear", () => SetContents(() => createDrawableFruit(0, true)));
AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawableFruit(rep, true))); AddStep("show hyperdash grape", () => SetContents(() => createDrawableFruit(1, true)));
AddStep("show hyperdash pineapple / apple", () => SetContents(() => createDrawableFruit(2, true)));
AddStep("show hyperdash raspberry / orange", () => SetContents(() => createDrawableFruit(3, true)));
AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true))); AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
} }
private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) => private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) =>
setProperties(new DrawableFruit(new TestCatchFruit(rep)), hyperdash); SetProperties(new DrawableFruit(new Fruit
{
IndexInBeatmap = indexInBeatmap,
HyperDashBindable = { Value = hyperdash }
}));
private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash); private Drawable createDrawableBanana() =>
SetProperties(new DrawableBanana(new Banana()));
private Drawable createDrawableTinyDroplet() => setProperties(new DrawableTinyDroplet(new TinyDroplet())); private Drawable createDrawableDroplet(bool hyperdash = false) =>
SetProperties(new DrawableDroplet(new Droplet
{
HyperDashBindable = { Value = hyperdash }
}));
private DrawableCatchHitObject setProperties(DrawableCatchHitObject d, bool hyperdash = false) private Drawable createDrawableTinyDroplet() => SetProperties(new DrawableTinyDroplet(new TinyDroplet()));
protected virtual DrawableCatchHitObject SetProperties(DrawableCatchHitObject d)
{ {
var hitObject = d.HitObject; var hitObject = d.HitObject;
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 0 });
hitObject.StartTime = 1000000000000; hitObject.StartTime = 1000000000000;
hitObject.Scale = 1.5f; hitObject.Scale = 1.5f;
if (hyperdash)
((PalpableCatchHitObject)hitObject).HyperDashTarget = new Banana();
d.Anchor = Anchor.Centre; d.Anchor = Anchor.Centre;
d.RelativePositionAxes = Axes.None; d.RelativePositionAxes = Axes.None;
d.Position = Vector2.Zero; d.Position = Vector2.Zero;
@ -55,15 +71,5 @@ namespace osu.Game.Rulesets.Catch.Tests
}; };
return d; return d;
} }
public class TestCatchFruit : Fruit
{
public TestCatchFruit(FruitVisualRepresentation rep)
{
VisualRepresentation = rep;
}
public override FruitVisualRepresentation VisualRepresentation { get; }
}
} }
} }

View File

@ -0,0 +1,32 @@
// 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.Bindables;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneFruitVisualChange : TestSceneFruitObjects
{
private readonly Bindable<int> indexInBeatmap = new Bindable<int>();
private readonly Bindable<bool> hyperDash = new Bindable<bool>();
protected override void LoadComplete()
{
AddStep("fruit changes visual and hyper", () => SetContents(() => SetProperties(new DrawableFruit(new Fruit
{
IndexInBeatmapBindable = { BindTarget = indexInBeatmap },
HyperDashBindable = { BindTarget = hyperDash },
}))));
AddStep("droplet changes hyper", () => SetContents(() => SetProperties(new DrawableDroplet(new Droplet
{
HyperDashBindable = { BindTarget = hyperDash },
}))));
Scheduler.AddDelayed(() => indexInBeatmap.Value++, 250, true);
Scheduler.AddDelayed(() => hyperDash.Value = !hyperDash.Value, 1000, true);
}
}
}

View File

@ -2,21 +2,22 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
{ {
public class Banana : Fruit public class Banana : Fruit, IHasComboInformation
{ {
/// <summary> /// <summary>
/// Index of banana in current shower. /// Index of banana in current shower.
/// </summary> /// </summary>
public int BananaIndex; public int BananaIndex;
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
public override Judgement CreateJudgement() => new CatchBananaJudgement(); public override Judgement CreateJudgement() => new CatchBananaJudgement();
private static readonly List<HitSampleInfo> samples = new List<HitSampleInfo> { new BananaHitSampleInfo() }; private static readonly List<HitSampleInfo> samples = new List<HitSampleInfo> { new BananaHitSampleInfo() };
@ -26,6 +27,29 @@ namespace osu.Game.Rulesets.Catch.Objects
Samples = samples; Samples = samples;
} }
private Color4? colour;
Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours)
{
// override any external colour changes with banananana
return colour ??= getBananaColour();
}
private Color4 getBananaColour()
{
switch (RNG.Next(0, 3))
{
default:
return new Color4(255, 240, 0, 255);
case 1:
return new Color4(255, 192, 0, 255);
case 2:
return new Color4(214, 221, 28, 255);
}
}
private class BananaHitSampleInfo : HitSampleInfo private class BananaHitSampleInfo : HitSampleInfo
{ {
private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" }; private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" };

View File

@ -9,8 +9,6 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
public class BananaShower : CatchHitObject, IHasDuration public class BananaShower : CatchHitObject, IHasDuration
{ {
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
public override bool LastInCombo => true; public override bool LastInCombo => true;
public override Judgement CreateJudgement() => new IgnoreJudgement(); public override Judgement CreateJudgement() => new IgnoreJudgement();

View File

@ -16,27 +16,47 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
public const float OBJECT_RADIUS = 64; public const float OBJECT_RADIUS = 64;
private float x; // This value is after XOffset applied.
public readonly Bindable<float> XBindable = new Bindable<float>();
// This value is before XOffset applied.
private float originalX;
/// <summary> /// <summary>
/// The horizontal position of the fruit between 0 and <see cref="CatchPlayfield.WIDTH"/>. /// The horizontal position of the fruit between 0 and <see cref="CatchPlayfield.WIDTH"/>.
/// </summary> /// </summary>
public float X public float X
{ {
get => x + XOffset; // TODO: I don't like this asymmetry.
set => x = value; get => XBindable.Value;
// originalX is set by `XBindable.BindValueChanged`
set => XBindable.Value = value + xOffset;
} }
private float xOffset;
/// <summary> /// <summary>
/// A random offset applied to <see cref="X"/>, set by the <see cref="CatchBeatmapProcessor"/>. /// A random offset applied to <see cref="X"/>, set by the <see cref="CatchBeatmapProcessor"/>.
/// </summary> /// </summary>
internal float XOffset { get; set; } internal float XOffset
{
get => xOffset;
set
{
xOffset = value;
XBindable.Value = originalX + xOffset;
}
}
public double TimePreempt = 1000; public double TimePreempt = 1000;
public int IndexInBeatmap { get; set; } public readonly Bindable<int> IndexInBeatmapBindable = new Bindable<int>();
public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(IndexInBeatmap % 4); public int IndexInBeatmap
{
get => IndexInBeatmapBindable.Value;
set => IndexInBeatmapBindable.Value = value;
}
public virtual bool NewCombo { get; set; } public virtual bool NewCombo { get; set; }
@ -69,7 +89,13 @@ namespace osu.Game.Rulesets.Catch.Objects
set => LastInComboBindable.Value = value; set => LastInComboBindable.Value = value;
} }
public float Scale { get; set; } = 1; public readonly Bindable<float> ScaleBindable = new Bindable<float>(1);
public float Scale
{
get => ScaleBindable.Value;
set => ScaleBindable.Value = value;
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{ {
@ -81,14 +107,10 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
public enum FruitVisualRepresentation protected CatchHitObject()
{ {
Pear, XBindable.BindValueChanged(x => originalX = x.NewValue - xOffset);
Grape, }
Pineapple,
Raspberry,
Banana // banananananannaanana
} }
} }

View File

@ -1,28 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableBanana : DrawableFruit public class DrawableBanana : DrawableFruit
{ {
protected override FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => FruitVisualRepresentation.Banana;
public DrawableBanana(Banana h) public DrawableBanana(Banana h)
: base(h) : base(h)
{ {
} }
private Color4? colour;
protected override Color4 GetComboColour(IReadOnlyList<Color4> comboColours)
{
// override any external colour changes with banananana
return colour ??= getBananaColour();
}
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()
{ {
base.UpdateInitialTransforms(); base.UpdateInitialTransforms();
@ -46,20 +38,5 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
if (Samples != null) if (Samples != null)
Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f; Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f;
} }
private Color4 getBananaColour()
{
switch (RNG.Next(0, 3))
{
default:
return new Color4(255, 240, 0, 255);
case 1:
return new Color4(255, 192, 0, 255);
case 2:
return new Color4(214, 221, 28, 255);
}
}
} }
} }

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -10,19 +12,34 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject> public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject>
{ {
public readonly Bindable<float> XBindable = new Bindable<float>();
protected override double InitialLifetimeOffset => HitObject.TimePreempt; protected override double InitialLifetimeOffset => HitObject.TimePreempt;
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
protected DrawableCatchHitObject(CatchHitObject hitObject) protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject)
: base(hitObject) : base(hitObject)
{ {
X = hitObject.X;
Anchor = Anchor.BottomLeft; Anchor = Anchor.BottomLeft;
} }
protected override void OnApply()
{
base.OnApply();
XBindable.BindTo(HitObject.XBindable);
}
protected override void OnFree()
{
base.OnFree();
XBindable.UnbindFrom(HitObject.XBindable);
}
public Func<CatchHitObject, bool> CheckPosition; public Func<CatchHitObject, bool> CheckPosition;
public bool IsOnPlate; public bool IsOnPlate;

View File

@ -21,7 +21,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new DropletPiece()); HyperDash.BindValueChanged(_ => updatePiece(), true);
}
private void updatePiece()
{
ScaleContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Droplet),
_ => new DropletPiece
{
HyperDash = { BindTarget = HyperDash }
});
} }
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -11,6 +12,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableFruit : DrawablePalpableCatchHitObject public class DrawableFruit : DrawablePalpableCatchHitObject
{ {
public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
protected virtual FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4);
public DrawableFruit(CatchHitObject h) public DrawableFruit(CatchHitObject h)
: base(h) : base(h)
{ {
@ -19,10 +24,26 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
ScaleContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(getComponent(HitObject.VisualRepresentation)), _ => new FruitPiece());
ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40;
IndexInBeatmap.BindValueChanged(change =>
{
VisualRepresentation.Value = GetVisualRepresentation(change.NewValue);
}, true);
VisualRepresentation.BindValueChanged(_ => updatePiece());
HyperDash.BindValueChanged(_ => updatePiece(), true);
}
private void updatePiece()
{
ScaleContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(getComponent(VisualRepresentation.Value)),
_ => new FruitPiece
{
VisualRepresentation = { BindTarget = VisualRepresentation },
HyperDash = { BindTarget = HyperDash },
});
} }
private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation) private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation)
@ -49,4 +70,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
} }
} }
} }
public enum FruitVisualRepresentation
{
Pear,
Grape,
Pineapple,
Raspberry,
Banana // banananananannaanana
}
} }

View File

@ -1,12 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
@ -14,6 +14,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject; public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject;
public readonly Bindable<bool> HyperDash = new Bindable<bool>();
public readonly Bindable<float> ScaleBindable = new Bindable<float>(1);
public readonly Bindable<int> IndexInBeatmap = new Bindable<int>();
/// <summary>
/// The multiplicative factor applied to <see cref="ScaleContainer"/> scale relative to <see cref="HitObject"/> scale.
/// </summary>
protected virtual float ScaleFactor => 1;
/// <summary> /// <summary>
/// Whether this hit object should stay on the catcher plate when the object is caught by the catcher. /// Whether this hit object should stay on the catcher plate when the object is caught by the catcher.
/// </summary> /// </summary>
@ -21,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
protected readonly Container ScaleContainer; protected readonly Container ScaleContainer;
protected DrawablePalpableCatchHitObject(CatchHitObject h) protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h)
: base(h) : base(h)
{ {
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -38,10 +49,35 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
ScaleContainer.Scale = new Vector2(HitObject.Scale); XBindable.BindValueChanged(x =>
{
if (!IsOnPlate) X = x.NewValue;
}, true);
ScaleBindable.BindValueChanged(scale =>
{
ScaleContainer.Scale = new Vector2(scale.NewValue * ScaleFactor);
}, true);
IndexInBeatmap.BindValueChanged(_ => UpdateComboColour());
} }
protected override Color4 GetComboColour(IReadOnlyList<Color4> comboColours) => protected override void OnApply()
comboColours[(HitObject.IndexInBeatmap + 1) % comboColours.Count]; {
base.OnApply();
HyperDash.BindTo(HitObject.HyperDashBindable);
ScaleBindable.BindTo(HitObject.ScaleBindable);
IndexInBeatmap.BindTo(HitObject.IndexInBeatmapBindable);
}
protected override void OnFree()
{
HyperDash.UnbindFrom(HitObject.HyperDashBindable);
ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
IndexInBeatmap.UnbindFrom(HitObject.IndexInBeatmapBindable);
base.OnFree();
}
} }
} }

View File

@ -1,21 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableTinyDroplet : DrawableDroplet public class DrawableTinyDroplet : DrawableDroplet
{ {
protected override float ScaleFactor => base.ScaleFactor / 2;
public DrawableTinyDroplet(TinyDroplet h) public DrawableTinyDroplet(TinyDroplet h)
: base(h) : base(h)
{ {
} }
[BackgroundDependencyLoader]
private void load()
{
ScaleContainer.Scale /= 2;
}
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -11,6 +12,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
public class DropletPiece : CompositeDrawable public class DropletPiece : CompositeDrawable
{ {
public readonly Bindable<bool> HyperDash = new Bindable<bool>();
public DropletPiece() public DropletPiece()
{ {
Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2); Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
@ -19,15 +22,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject) private void load(DrawableHitObject drawableObject)
{ {
var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject;
InternalChild = new Pulp InternalChild = new Pulp
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
AccentColour = { BindTarget = drawableObject.AccentColour } AccentColour = { BindTarget = drawableObject.AccentColour }
}; };
if (drawableCatchObject.HitObject.HyperDash) if (HyperDash.Value)
{ {
AddInternal(new HyperDropletBorderPiece()); AddInternal(new HyperDropletBorderPiece());
} }

View File

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -16,36 +18,39 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
/// </summary> /// </summary>
public const float RADIUS_ADJUST = 1.1f; public const float RADIUS_ADJUST = 1.1f;
private BorderPiece border; public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
private PalpableCatchHitObject hitObject; public readonly Bindable<bool> HyperDash = new Bindable<bool>();
[CanBeNull]
private DrawableCatchHitObject drawableHitObject;
[CanBeNull]
private BorderPiece borderPiece;
public FruitPiece() public FruitPiece()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader(permitNulls: true)]
private void load(DrawableHitObject drawableObject) private void load([CanBeNull] DrawableHitObject drawable)
{ {
var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject; drawableHitObject = (DrawableCatchHitObject)drawable;
hitObject = drawableCatchObject.HitObject;
AddRangeInternal(new[] AddInternal(getFruitFor(VisualRepresentation.Value));
{
getFruitFor(hitObject.VisualRepresentation),
border = new BorderPiece(),
});
if (hitObject.HyperDash) // if it is not part of a DHO, the border is always invisible.
{ if (drawableHitObject != null)
AddInternal(borderPiece = new BorderPiece());
if (HyperDash.Value)
AddInternal(new HyperBorderPiece()); AddInternal(new HyperBorderPiece());
}
} }
protected override void Update() protected override void Update()
{ {
base.Update(); if (borderPiece != null && drawableHitObject?.HitObject != null)
border.Alpha = (float)Math.Clamp((hitObject.StartTime - Time.Current) / 500, 0, 1); borderPiece.Alpha = (float)Math.Clamp((drawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1);
} }
private Drawable getFruitFor(FruitVisualRepresentation representation) private Drawable getFruitFor(FruitVisualRepresentation representation)

View File

@ -1,13 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
{ {
/// <summary> /// <summary>
/// Represents a single object that can be caught by the catcher. /// Represents a single object that can be caught by the catcher.
/// This includes normal fruits, droplets, and bananas but excludes objects that act only as a container of nested hit objects. /// This includes normal fruits, droplets, and bananas but excludes objects that act only as a container of nested hit objects.
/// </summary> /// </summary>
public abstract class PalpableCatchHitObject : CatchHitObject public abstract class PalpableCatchHitObject : CatchHitObject, IHasComboInformation
{ {
/// <summary> /// <summary>
/// Difference between the distance to the next object /// Difference between the distance to the next object
@ -16,14 +21,28 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary> /// </summary>
public float DistanceToHyperDash { get; set; } public float DistanceToHyperDash { get; set; }
public readonly Bindable<bool> HyperDashBindable = new Bindable<bool>();
/// <summary> /// <summary>
/// Whether this fruit can initiate a hyperdash. /// Whether this fruit can initiate a hyperdash.
/// </summary> /// </summary>
public bool HyperDash => HyperDashTarget != null; public bool HyperDash => HyperDashBindable.Value;
private CatchHitObject hyperDashTarget;
/// <summary> /// <summary>
/// The target fruit if we are to initiate a hyperdash. /// The target fruit if we are to initiate a hyperdash.
/// </summary> /// </summary>
public CatchHitObject HyperDashTarget; public CatchHitObject HyperDashTarget
{
get => hyperDashTarget;
set
{
hyperDashTarget = value;
HyperDashBindable.Value = value != null;
}
}
Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count];
} }
} }

View File

@ -55,7 +55,13 @@ namespace osu.Game.Rulesets.Catch.UI
HitObjectContainer, HitObjectContainer,
CatcherArea, CatcherArea,
}; };
}
protected override void LoadComplete()
{
base.LoadComplete();
// these subscriptions need to be done post constructor to ensure externally bound components have a chance to populate required fields (ScoreProcessor / ComboAtJudgement in this case).
NewResult += onNewResult; NewResult += onNewResult;
RevertResult += onRevertResult; RevertResult += onRevertResult;
} }

View File

@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.UI
DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject; DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject;
maniaObject.CheckHittable = hitPolicy.IsHittable; maniaObject.CheckHittable = hitPolicy.IsHittable;
HitObjectContainer.Add(hitObject); base.Add(hitObject);
} }
public override bool Remove(DrawableHitObject h) public override bool Remove(DrawableHitObject h)

View File

@ -53,9 +53,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}); });
} }
protected override void OnApply(HitObject hitObject) protected override void OnApply()
{ {
base.OnApply(hitObject); base.OnApply();
IndexInCurrentComboBindable.BindTo(HitObject.IndexInCurrentComboBindable); IndexInCurrentComboBindable.BindTo(HitObject.IndexInCurrentComboBindable);
PositionBindable.BindTo(HitObject.PositionBindable); PositionBindable.BindTo(HitObject.PositionBindable);
@ -70,9 +70,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000; LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000;
} }
protected override void OnFree(HitObject hitObject) protected override void OnFree()
{ {
base.OnFree(hitObject); base.OnFree();
IndexInCurrentComboBindable.UnbindFrom(HitObject.IndexInCurrentComboBindable); IndexInCurrentComboBindable.UnbindFrom(HitObject.IndexInCurrentComboBindable);
PositionBindable.UnbindFrom(HitObject.PositionBindable); PositionBindable.UnbindFrom(HitObject.PositionBindable);

View File

@ -86,18 +86,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.BindValueChanged(updateSlidingSample); Tracking.BindValueChanged(updateSlidingSample);
} }
protected override void OnApply(HitObject hitObject) protected override void OnApply()
{ {
base.OnApply(hitObject); base.OnApply();
// Ensure that the version will change after the upcoming BindTo(). // Ensure that the version will change after the upcoming BindTo().
pathVersion.Value = int.MaxValue; pathVersion.Value = int.MaxValue;
PathVersion.BindTo(HitObject.Path.Version); PathVersion.BindTo(HitObject.Path.Version);
} }
protected override void OnFree(HitObject hitObject) protected override void OnFree()
{ {
base.OnFree(hitObject); base.OnFree();
PathVersion.UnbindFrom(HitObject.Path.Version); PathVersion.UnbindFrom(HitObject.Path.Version);
} }

View File

@ -4,7 +4,6 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -36,9 +35,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
pathVersion.BindValueChanged(_ => updatePosition()); pathVersion.BindValueChanged(_ => updatePosition());
} }
protected override void OnFree(HitObject hitObject) protected override void OnFree()
{ {
base.OnFree(hitObject); base.OnFree();
pathVersion.UnbindFrom(drawableSlider.PathVersion); pathVersion.UnbindFrom(drawableSlider.PathVersion);
} }

View File

@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}; };
} }
private readonly IBindable<ArmedState> state = new Bindable<ArmedState>();
private readonly IBindable<Color4> accentColour = new Bindable<Color4>(); private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>(); private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
@ -50,7 +49,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
var drawableOsuObject = (DrawableOsuHitObject)drawableObject; var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
state.BindTo(drawableObject.State);
accentColour.BindTo(drawableObject.AccentColour); accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
} }
@ -59,7 +57,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
base.LoadComplete(); base.LoadComplete();
state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour => accentColour.BindValueChanged(colour =>
{ {
explode.Colour = colour.NewValue; explode.Colour = colour.NewValue;
@ -68,15 +65,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}, true); }, true);
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true); indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
drawableObject.ApplyCustomUpdateState += updateState;
updateState(drawableObject, drawableObject.State.Value);
} }
private void updateState(ValueChangedEvent<ArmedState> state) private void updateState(DrawableHitObject drawableObject, ArmedState state)
{ {
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true)) using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true))
{ {
glow.FadeOut(400); glow.FadeOut(400);
switch (state.NewValue) switch (state)
{ {
case ArmedState.Hit: case ArmedState.Hit:
const double flash_in = 40; const double flash_in = 40;

View File

@ -248,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
} }
private void trackingChanged(ValueChangedEvent<bool> tracking) => private void trackingChanged(ValueChangedEvent<bool> tracking) =>
box.FadeTo(tracking.NewValue ? 0.6f : 0.05f, 200, Easing.OutQuint); box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint);
} }
} }
} }

View File

@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
private SkinnableSpriteText hitCircleText; private SkinnableSpriteText hitCircleText;
private readonly IBindable<ArmedState> state = new Bindable<ArmedState>();
private readonly Bindable<Color4> accentColour = new Bindable<Color4>(); private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>(); private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
@ -113,7 +112,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
if (overlayAboveNumber) if (overlayAboveNumber)
AddInternal(hitCircleOverlay.CreateProxy()); AddInternal(hitCircleOverlay.CreateProxy());
state.BindTo(drawableObject.State);
accentColour.BindTo(drawableObject.AccentColour); accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
@ -137,19 +135,21 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
base.LoadComplete(); base.LoadComplete();
state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
if (hasNumber) if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
drawableObject.ApplyCustomUpdateState += updateState;
updateState(drawableObject, drawableObject.State.Value);
} }
private void updateState(ValueChangedEvent<ArmedState> state) private void updateState(DrawableHitObject drawableObject, ArmedState state)
{ {
const double legacy_fade_duration = 240; const double legacy_fade_duration = 240;
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true)) using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true))
{ {
switch (state.NewValue) switch (state)
{ {
case ArmedState.Hit: case ArmedState.Hit:
circleSprites.FadeOut(legacy_fade_duration, Easing.Out); circleSprites.FadeOut(legacy_fade_duration, Easing.Out);

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -37,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private SkinnableDrawable mascot; private SkinnableDrawable mascot;
private ProxyContainer topLevelHitContainer; private ProxyContainer topLevelHitContainer;
private ProxyContainer barlineContainer; private ScrollingHitObjectContainer barlineContainer;
private Container rightArea; private Container rightArea;
private Container leftArea; private Container leftArea;
@ -83,10 +84,7 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
barlineContainer = new ProxyContainer barlineContainer = new ScrollingHitObjectContainer(),
{
RelativeSizeAxes = Axes.Both,
},
new Container new Container
{ {
Name = "Hit objects", Name = "Hit objects",
@ -159,18 +157,37 @@ namespace osu.Game.Rulesets.Taiko.UI
public override void Add(DrawableHitObject h) public override void Add(DrawableHitObject h)
{ {
h.OnNewResult += OnNewResult;
base.Add(h);
switch (h) switch (h)
{ {
case DrawableBarLine barline: case DrawableBarLine barline:
barlineContainer.Add(barline.CreateProxy()); barlineContainer.Add(barline);
break; break;
case DrawableTaikoHitObject taikoObject: case DrawableTaikoHitObject taikoObject:
h.OnNewResult += OnNewResult;
topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); topLevelHitContainer.Add(taikoObject.CreateProxiedContent());
base.Add(h);
break; break;
default:
throw new ArgumentException($"Unsupported {nameof(DrawableHitObject)} type");
}
}
public override bool Remove(DrawableHitObject h)
{
switch (h)
{
case DrawableBarLine barline:
return barlineContainer.Remove(barline);
case DrawableTaikoHitObject _:
h.OnNewResult -= OnNewResult;
// todo: consider tidying of proxied content if required.
return base.Remove(h);
default:
throw new ArgumentException($"Unsupported {nameof(DrawableHitObject)} type");
} }
} }

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
@ -44,6 +43,36 @@ namespace osu.Game.Tests.Editing
Assert.That(stateChangedFired, Is.EqualTo(2)); Assert.That(stateChangedFired, Is.EqualTo(2));
} }
[Test]
public void TestApplyThenUndoThenApplySameChange()
{
var (handler, beatmap) = createChangeHandler();
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False);
string originalHash = handler.CurrentStateHash;
addArbitraryChange(beatmap);
handler.SaveState();
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
Assert.That(stateChangedFired, Is.EqualTo(1));
string hash = handler.CurrentStateHash;
// undo a change without saving
handler.RestoreState(-1);
Assert.That(originalHash, Is.EqualTo(handler.CurrentStateHash));
Assert.That(stateChangedFired, Is.EqualTo(2));
addArbitraryChange(beatmap);
handler.SaveState();
Assert.That(hash, Is.EqualTo(handler.CurrentStateHash));
}
[Test] [Test]
public void TestSaveSameStateDoesNotSave() public void TestSaveSameStateDoesNotSave()
{ {
@ -139,7 +168,7 @@ namespace osu.Game.Tests.Editing
private void addArbitraryChange(EditorBeatmap beatmap) private void addArbitraryChange(EditorBeatmap beatmap)
{ {
beatmap.Add(new HitCircle { StartTime = RNG.Next(0, 100000) }); beatmap.Add(new HitCircle { StartTime = 2760 });
} }
} }
} }

View File

@ -21,13 +21,13 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using JetBrains.Annotations;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -46,6 +46,50 @@ namespace osu.Game.Tests.Visual.Gameplay
[SetUp] [SetUp]
public void Setup() => Schedule(() => testClock.CurrentTime = 0); public void Setup() => Schedule(() => testClock.CurrentTime = 0);
[TestCase("pooled")]
[TestCase("non-pooled")]
public void TestHitObjectLifetime(string pooled)
{
var beatmap = createBeatmap(_ => pooled == "pooled" ? new TestPooledHitObject() : new TestHitObject());
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
createTest(beatmap);
assertPosition(0, 0f);
assertDead(3);
setTime(3 * time_range);
assertPosition(3, 0f);
assertDead(0);
setTime(0 * time_range);
assertPosition(0, 0f);
assertDead(3);
}
[TestCase("pooled")]
[TestCase("non-pooled")]
public void TestNestedHitObject(string pooled)
{
var beatmap = createBeatmap(i =>
{
var h = pooled == "pooled" ? new TestPooledParentHitObject() : new TestParentHitObject();
h.Duration = 300;
h.ChildTimeOffset = i % 3 * 100;
return h;
});
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
createTest(beatmap);
assertPosition(0, 0f);
assertHeight(0);
assertChildPosition(0);
setTime(5 * time_range);
assertPosition(5, 0f);
assertHeight(5);
assertChildPosition(5);
}
[Test] [Test]
public void TestRelativeBeatLengthScaleSingleTimingPoint() public void TestRelativeBeatLengthScaleSingleTimingPoint()
{ {
@ -147,8 +191,37 @@ namespace osu.Game.Tests.Visual.Gameplay
assertPosition(1, 1); assertPosition(1, 1);
} }
/// <summary>
/// Get a <see cref="DrawableTestHitObject" /> corresponding to the <paramref name="index"/>'th <see cref="TestHitObject"/>.
/// When the hit object is not alive, `null` is returned.
/// </summary>
[CanBeNull]
private DrawableTestHitObject getDrawableHitObject(int index)
{
var hitObject = drawableRuleset.Beatmap.HitObjects.ElementAt(index);
return (DrawableTestHitObject)drawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(obj => obj.HitObject == hitObject);
}
private float yScale => drawableRuleset.Playfield.HitObjectContainer.DrawHeight;
private void assertDead(int index) => AddAssert($"hitobject {index} is dead", () => getDrawableHitObject(index) == null);
private void assertHeight(int index) => AddAssert($"hitobject {index} height", () =>
{
var d = getDrawableHitObject(index);
return d != null && Precision.AlmostEquals(d.DrawHeight, yScale * (float)(d.HitObject.Duration / time_range), 0.1f);
});
private void assertChildPosition(int index) => AddAssert($"hitobject {index} child position", () =>
{
var d = getDrawableHitObject(index);
return d is DrawableTestParentHitObject && Precision.AlmostEquals(
d.NestedHitObjects.First().DrawPosition.Y,
yScale * (float)((TestParentHitObject)d.HitObject).ChildTimeOffset / time_range, 0.1f);
});
private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}", private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}",
() => Precision.AlmostEquals(drawableRuleset.Playfield.AllHitObjects.ElementAt(index).DrawPosition.Y, drawableRuleset.Playfield.HitObjectContainer.DrawHeight * relativeY)); () => Precision.AlmostEquals(getDrawableHitObject(index)?.DrawPosition.Y ?? -1, yScale * relativeY));
private void setTime(double time) private void setTime(double time)
{ {
@ -160,12 +233,16 @@ namespace osu.Game.Tests.Visual.Gameplay
/// The hitobjects are spaced <see cref="time_range"/> milliseconds apart. /// The hitobjects are spaced <see cref="time_range"/> milliseconds apart.
/// </summary> /// </summary>
/// <returns>The <see cref="IBeatmap"/>.</returns> /// <returns>The <see cref="IBeatmap"/>.</returns>
private IBeatmap createBeatmap() private IBeatmap createBeatmap(Func<int, TestHitObject> createAction = null)
{ {
var beatmap = new Beatmap<HitObject> { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } }; var beatmap = new Beatmap<TestHitObject> { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } };
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
beatmap.HitObjects.Add(new HitObject { StartTime = i * time_range }); {
var h = createAction?.Invoke(i) ?? new TestHitObject();
h.StartTime = i * time_range;
beatmap.HitObjects.Add(h);
}
return beatmap; return beatmap;
} }
@ -225,7 +302,21 @@ namespace osu.Game.Tests.Visual.Gameplay
TimeRange.Value = time_range; TimeRange.Value = time_range;
} }
public override DrawableHitObject<TestHitObject> CreateDrawableRepresentation(TestHitObject h) => new DrawableTestHitObject(h); public override DrawableHitObject<TestHitObject> CreateDrawableRepresentation(TestHitObject h)
{
switch (h)
{
case TestPooledHitObject _:
case TestPooledParentHitObject _:
return null;
case TestParentHitObject p:
return new DrawableTestParentHitObject(p);
default:
return new DrawableTestHitObject(h);
}
}
protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager();
@ -265,6 +356,9 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
} }
}); });
RegisterPool<TestPooledHitObject, DrawableTestPooledHitObject>(1);
RegisterPool<TestPooledParentHitObject, DrawableTestPooledParentHitObject>(1);
} }
} }
@ -277,30 +371,46 @@ namespace osu.Game.Tests.Visual.Gameplay
public override bool CanConvert() => true; public override bool CanConvert() => true;
protected override IEnumerable<TestHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) protected override IEnumerable<TestHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) =>
{ throw new NotImplementedException();
yield return new TestHitObject
{
StartTime = original.StartTime,
Duration = (original as IHasDuration)?.Duration ?? 100
};
}
} }
#endregion #endregion
#region HitObject #region HitObject
private class TestHitObject : ConvertHitObject, IHasDuration private class TestHitObject : HitObject, IHasDuration
{ {
public double EndTime => StartTime + Duration; public double EndTime => StartTime + Duration;
public double Duration { get; set; } public double Duration { get; set; } = 100;
}
private class TestPooledHitObject : TestHitObject
{
}
private class TestParentHitObject : TestHitObject
{
public double ChildTimeOffset;
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
AddNested(new TestHitObject { StartTime = StartTime + ChildTimeOffset });
}
}
private class TestPooledParentHitObject : TestParentHitObject
{
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
AddNested(new TestPooledHitObject { StartTime = StartTime + ChildTimeOffset });
}
} }
private class DrawableTestHitObject : DrawableHitObject<TestHitObject> private class DrawableTestHitObject : DrawableHitObject<TestHitObject>
{ {
public DrawableTestHitObject(TestHitObject hitObject) public DrawableTestHitObject([CanBeNull] TestHitObject hitObject)
: base(hitObject) : base(hitObject)
{ {
Anchor = Anchor.TopCentre; Anchor = Anchor.TopCentre;
@ -324,6 +434,52 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
}); });
} }
protected override void Update() => LifetimeEnd = HitObject.EndTime;
}
private class DrawableTestPooledHitObject : DrawableTestHitObject
{
public DrawableTestPooledHitObject()
: base(null)
{
InternalChildren[0].Colour = Color4.LightSkyBlue;
InternalChildren[1].Colour = Color4.Blue;
}
}
private class DrawableTestParentHitObject : DrawableTestHitObject
{
private readonly Container<DrawableHitObject> container;
public DrawableTestParentHitObject([CanBeNull] TestHitObject hitObject)
: base(hitObject)
{
InternalChildren[0].Colour = Color4.LightYellow;
InternalChildren[1].Colour = Color4.Yellow;
AddInternal(container = new Container<DrawableHitObject>
{
RelativeSizeAxes = Axes.Both,
});
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) =>
new DrawableTestHitObject((TestHitObject)hitObject);
protected override void AddNestedHitObject(DrawableHitObject hitObject) => container.Add(hitObject);
protected override void ClearNestedHitObjects() => container.Clear(false);
}
private class DrawableTestPooledParentHitObject : DrawableTestParentHitObject
{
public DrawableTestPooledParentHitObject()
: base(null)
{
InternalChildren[0].Colour = Color4.LightSeaGreen;
InternalChildren[1].Colour = Color4.Green;
}
} }
#endregion #endregion

View File

@ -261,9 +261,9 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
} }
protected override void OnApply(HitObject hitObject) protected override void OnApply()
{ {
base.OnApply(hitObject); base.OnApply();
Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200)); Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200));
} }

View File

@ -3,7 +3,6 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -14,44 +13,41 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestFixture] [TestFixture]
public class TestSceneStarCounter : OsuTestScene public class TestSceneStarCounter : OsuTestScene
{ {
private readonly StarCounter starCounter;
private readonly OsuSpriteText starsLabel;
public TestSceneStarCounter() public TestSceneStarCounter()
{ {
StarCounter stars = new StarCounter starCounter = new StarCounter
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Current = 5,
}; };
Add(stars); Add(starCounter);
SpriteText starsLabel = new OsuSpriteText starsLabel = new OsuSpriteText
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Scale = new Vector2(2), Scale = new Vector2(2),
Y = 50, Y = 50,
Text = stars.Current.ToString("0.00"),
}; };
Add(starsLabel); Add(starsLabel);
AddRepeatStep(@"random value", delegate setStars(5);
{
stars.Current = RNG.NextSingle() * (stars.StarCount + 1);
starsLabel.Text = stars.Current.ToString("0.00");
}, 10);
AddStep(@"Stop animation", delegate AddRepeatStep("random value", () => setStars(RNG.NextSingle() * (starCounter.StarCount + 1)), 10);
{ AddSliderStep("exact value", 0f, 10f, 5f, setStars);
stars.StopAnimation(); AddStep("stop animation", () => starCounter.StopAnimation());
}); AddStep("reset", () => setStars(0));
}
AddStep(@"Reset", delegate private void setStars(float stars)
{ {
stars.Current = 0; starCounter.Current = stars;
starsLabel.Text = stars.Current.ToString("0.00"); starsLabel.Text = starCounter.Current.ToString("0.00");
});
} }
} }
} }

View File

@ -917,7 +917,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
get get
{ {
foreach (var item in ScrollableContent) foreach (var item in Scroll.Children)
{ {
yield return item; yield return item;

View File

@ -12,7 +12,19 @@ using osuTK.Input;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
{ {
public class OsuScrollContainer : ScrollContainer<Drawable> public class OsuScrollContainer : OsuScrollContainer<Drawable>
{
public OsuScrollContainer()
{
}
public OsuScrollContainer(Direction direction)
: base(direction)
{
}
}
public class OsuScrollContainer<T> : ScrollContainer<T> where T : Drawable
{ {
public const float SCROLL_BAR_HEIGHT = 10; public const float SCROLL_BAR_HEIGHT = 10;
public const float SCROLL_BAR_PADDING = 3; public const float SCROLL_BAR_PADDING = 3;

View File

@ -147,7 +147,7 @@ namespace osu.Game.Graphics.UserInterface
public override void DisplayAt(float scale) public override void DisplayAt(float scale)
{ {
scale = Math.Clamp(scale, min_star_scale, 1); scale = (float)Interpolation.Lerp(min_star_scale, 1, Math.Clamp(scale, 0, 1));
this.FadeTo(scale, fading_duration); this.FadeTo(scale, fading_duration);
Icon.ScaleTo(scale, scaling_duration, scaling_easing); Icon.ScaleTo(scale, scaling_duration, scaling_easing);

View File

@ -69,6 +69,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed),
new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed),
new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
new KeyBinding(InputKey.Space, GlobalAction.TogglePauseReplay),
new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD),
}; };
@ -163,10 +164,10 @@ namespace osu.Game.Input.Bindings
[Description("Toggle now playing overlay")] [Description("Toggle now playing overlay")]
ToggleNowPlaying, ToggleNowPlaying,
[Description("Previous Selection")] [Description("Previous selection")]
SelectPrevious, SelectPrevious,
[Description("Next Selection")] [Description("Next selection")]
SelectNext, SelectNext,
[Description("Home")] [Description("Home")]
@ -175,26 +176,29 @@ namespace osu.Game.Input.Bindings
[Description("Toggle notifications")] [Description("Toggle notifications")]
ToggleNotifications, ToggleNotifications,
[Description("Pause")] [Description("Pause gameplay")]
PauseGameplay, PauseGameplay,
// Editor // Editor
[Description("Setup Mode")] [Description("Setup mode")]
EditorSetupMode, EditorSetupMode,
[Description("Compose Mode")] [Description("Compose mode")]
EditorComposeMode, EditorComposeMode,
[Description("Design Mode")] [Description("Design mode")]
EditorDesignMode, EditorDesignMode,
[Description("Timing Mode")] [Description("Timing mode")]
EditorTimingMode, EditorTimingMode,
[Description("Hold for HUD")] [Description("Hold for HUD")]
HoldForHUD, HoldForHUD,
[Description("Random Skin")] [Description("Random skin")]
RandomSkin, RandomSkin,
[Description("Pause / resume replay")]
TogglePauseReplay,
} }
} }

View File

@ -27,7 +27,6 @@ namespace osu.Game.Overlays.Settings.Sections
new AudioDevicesSettings(), new AudioDevicesSettings(),
new VolumeSettings(), new VolumeSettings(),
new OffsetSettings(), new OffsetSettings(),
new MainMenuSettings()
}; };
} }
} }

View File

@ -26,7 +26,6 @@ namespace osu.Game.Overlays.Settings.Sections
Children = new Drawable[] Children = new Drawable[]
{ {
new GeneralSettings(), new GeneralSettings(),
new SongSelectSettings(),
new ModsSettings(), new ModsSettings(),
}; };
} }

View File

@ -23,7 +23,6 @@ namespace osu.Game.Overlays.Settings.Sections
new RendererSettings(), new RendererSettings(),
new LayoutSettings(), new LayoutSettings(),
new DetailSettings(), new DetailSettings(),
new UserInterfaceSettings(),
}; };
} }
} }

View File

@ -0,0 +1,15 @@
// 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.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings.Sections
{
/// <summary>
/// A slider intended to show a "size" multiplier number, where 1x is 1.0.
/// </summary>
internal class SizeSlider : OsuSliderBar<float>
{
public override string TooltipText => Current.Value.ToString(@"0.##x");
}
}

View File

@ -54,12 +54,6 @@ namespace osu.Game.Overlays.Settings.Sections
skinDropdown = new SkinSettingsDropdown(), skinDropdown = new SkinSettingsDropdown(),
new ExportSkinButton(), new ExportSkinButton(),
new SettingsSlider<float, SizeSlider> new SettingsSlider<float, SizeSlider>
{
LabelText = "Menu cursor size",
Current = config.GetBindable<float>(OsuSetting.MenuCursorSize),
KeyboardStep = 0.01f
},
new SettingsSlider<float, SizeSlider>
{ {
LabelText = "Gameplay cursor size", LabelText = "Gameplay cursor size",
Current = config.GetBindable<float>(OsuSetting.GameplayCursorSize), Current = config.GetBindable<float>(OsuSetting.GameplayCursorSize),
@ -136,11 +130,6 @@ namespace osu.Game.Overlays.Settings.Sections
Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray()); Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray());
} }
private class SizeSlider : OsuSliderBar<float>
{
public override string TooltipText => Current.Value.ToString(@"0.##x");
}
private class SkinSettingsDropdown : SettingsDropdown<SkinInfo> private class SkinSettingsDropdown : SettingsDropdown<SkinInfo>
{ {
protected override OsuDropdown<SkinInfo> CreateDropdown() => new SkinDropdownControl(); protected override OsuDropdown<SkinInfo> CreateDropdown() => new SkinDropdownControl();

View File

@ -6,11 +6,11 @@ using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings.Sections.Graphics namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
public class UserInterfaceSettings : SettingsSubsection public class GeneralSettings : SettingsSubsection
{ {
protected override string Header => "User Interface"; protected override string Header => "General";
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
@ -22,6 +22,12 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
LabelText = "Rotate cursor when dragging", LabelText = "Rotate cursor when dragging",
Current = config.GetBindable<bool>(OsuSetting.CursorRotation) Current = config.GetBindable<bool>(OsuSetting.CursorRotation)
}, },
new SettingsSlider<float, SizeSlider>
{
LabelText = "Menu cursor size",
Current = config.GetBindable<float>(OsuSetting.MenuCursorSize),
KeyboardStep = 0.01f
},
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Parallax", LabelText = "Parallax",

View File

@ -7,7 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
namespace osu.Game.Overlays.Settings.Sections.Audio namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
public class MainMenuSettings : SettingsSubsection public class MainMenuSettings : SettingsSubsection
{ {

View File

@ -8,7 +8,7 @@ using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings.Sections.Gameplay namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
public class SongSelectSettings : SettingsSubsection public class SongSelectSettings : SettingsSubsection
{ {

View File

@ -0,0 +1,29 @@
// 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.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Settings.Sections.UserInterface;
namespace osu.Game.Overlays.Settings.Sections
{
public class UserInterfaceSection : SettingsSection
{
public override string Header => "User Interface";
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Solid.LayerGroup
};
public UserInterfaceSection()
{
Children = new Drawable[]
{
new GeneralSettings(),
new MainMenuSettings(),
new SongSelectSettings()
};
}
}
}

View File

@ -23,10 +23,11 @@ namespace osu.Game.Overlays
{ {
new GeneralSection(), new GeneralSection(),
new GraphicsSection(), new GraphicsSection(),
new GameplaySection(),
new AudioSection(), new AudioSection(),
new SkinSection(),
new InputSection(createSubPanel(new KeyBindingPanel())), new InputSection(createSubPanel(new KeyBindingPanel())),
new UserInterfaceSection(),
new GameplaySection(),
new SkinSection(),
new OnlineSection(), new OnlineSection(),
new MaintenanceSection(), new MaintenanceSection(),
new DebugSection(), new DebugSection(),

View File

@ -32,9 +32,6 @@ namespace osu.Game.Rulesets.Judgements
private readonly Container aboveHitObjectsContent; private readonly Container aboveHitObjectsContent;
[Resolved]
private ISkinSource skinSource { get; set; }
/// <summary> /// <summary>
/// Duration of initial fade in. /// Duration of initial fade in.
/// </summary> /// </summary>
@ -78,29 +75,6 @@ namespace osu.Game.Rulesets.Judgements
public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy(); public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy();
protected override void LoadComplete()
{
base.LoadComplete();
skinSource.SourceChanged += onSkinChanged;
}
private void onSkinChanged()
{
// on a skin change, the child component will update but not get correctly triggered to play its animation.
// we need to trigger a reinitialisation to make things right.
currentDrawableType = null;
PrepareForUse();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (skinSource != null)
skinSource.SourceChanged -= onSkinChanged;
}
/// <summary> /// <summary>
/// Apply top-level animations to the current judgement when successfully hit. /// Apply top-level animations to the current judgement when successfully hit.
/// If displaying components which require lifetime extensions, manually adjusting <see cref="Drawable.LifetimeEnd"/> is required. /// If displaying components which require lifetime extensions, manually adjusting <see cref="Drawable.LifetimeEnd"/> is required.
@ -142,13 +116,14 @@ namespace osu.Game.Rulesets.Judgements
Debug.Assert(Result != null); Debug.Assert(Result != null);
prepareDrawables();
runAnimation(); runAnimation();
} }
private void runAnimation() private void runAnimation()
{ {
// is a no-op if the drawables are already in a correct state.
prepareDrawables();
// undo any transforms applies in ApplyMissAnimations/ApplyHitAnimations to get a sane initial state. // undo any transforms applies in ApplyMissAnimations/ApplyHitAnimations to get a sane initial state.
ApplyTransformsAt(double.MinValue, true); ApplyTransformsAt(double.MinValue, true);
ClearTransforms(true); ClearTransforms(true);
@ -203,7 +178,6 @@ namespace osu.Game.Rulesets.Judgements
if (JudgementBody != null) if (JudgementBody != null)
RemoveInternal(JudgementBody); RemoveInternal(JudgementBody);
aboveHitObjectsContent.Clear();
AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent<HitResult>(type), _ => AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent<HitResult>(type), _ =>
CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling) CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling)
{ {
@ -211,14 +185,29 @@ namespace osu.Game.Rulesets.Judgements
Origin = Anchor.Centre, Origin = Anchor.Centre,
}); });
if (JudgementBody.Drawable is IAnimatableJudgement animatable) JudgementBody.OnSkinChanged += () =>
{ {
var proxiedContent = animatable.GetAboveHitObjectsProxiedContent(); // on a skin change, the child component will update but not get correctly triggered to play its animation (or proxy the newly created content).
if (proxiedContent != null) // we need to trigger a reinitialisation to make things right.
aboveHitObjectsContent.Add(proxiedContent); proxyContent();
} runAnimation();
};
proxyContent();
currentDrawableType = type; currentDrawableType = type;
void proxyContent()
{
aboveHitObjectsContent.Clear();
if (JudgementBody.Drawable is IAnimatableJudgement animatable)
{
var proxiedContent = animatable.GetAboveHitObjectsProxiedContent();
if (proxiedContent != null)
aboveHitObjectsContent.Add(proxiedContent);
}
}
} }
protected virtual Drawable CreateDefaultJudgement(HitResult result) => new DefaultJudgementPiece(result); protected virtual Drawable CreateDefaultJudgement(HitResult result) => new DefaultJudgementPiece(result);

View File

@ -128,6 +128,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
private readonly Bindable<ArmedState> state = new Bindable<ArmedState>(); private readonly Bindable<ArmedState> state = new Bindable<ArmedState>();
/// <summary>
/// The state of this <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// For pooled hitobjects, <see cref="ApplyCustomUpdateState"/> is recommended to be used instead for better editor/rewinding support.
/// </remarks>
public IBindable<ArmedState> State => state; public IBindable<ArmedState> State => state;
/// <summary> /// <summary>
@ -184,7 +190,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
{ {
base.LoadComplete(); base.LoadComplete();
comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); comboIndexBindable.BindValueChanged(_ => UpdateComboColour(), true);
updateState(ArmedState.Idle, true); updateState(ArmedState.Idle, true);
} }
@ -254,12 +260,19 @@ namespace osu.Game.Rulesets.Objects.Drawables
HitObject.DefaultsApplied += onDefaultsApplied; HitObject.DefaultsApplied += onDefaultsApplied;
OnApply(hitObject); OnApply();
HitObjectApplied?.Invoke(this); HitObjectApplied?.Invoke(this);
// If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates. // If not loaded, the state update happens in LoadComplete().
if (IsLoaded) if (IsLoaded)
Schedule(() => updateState(ArmedState.Idle, true)); {
if (Result.IsHit)
updateState(ArmedState.Hit, true);
else if (Result.HasResult)
updateState(ArmedState.Miss, true);
else
updateState(ArmedState.Idle, true);
}
hasHitObjectApplied = true; hasHitObjectApplied = true;
} }
@ -299,7 +312,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
HitObject.DefaultsApplied -= onDefaultsApplied; HitObject.DefaultsApplied -= onDefaultsApplied;
OnFree(HitObject); OnFree();
HitObject = null; HitObject = null;
Result = null; Result = null;
@ -324,16 +337,14 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary> /// <summary>
/// Invoked for this <see cref="DrawableHitObject"/> to take on any values from a newly-applied <see cref="HitObject"/>. /// Invoked for this <see cref="DrawableHitObject"/> to take on any values from a newly-applied <see cref="HitObject"/>.
/// </summary> /// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> being applied.</param> protected virtual void OnApply()
protected virtual void OnApply(HitObject hitObject)
{ {
} }
/// <summary> /// <summary>
/// Invoked for this <see cref="DrawableHitObject"/> to revert any values previously taken on from the currently-applied <see cref="HitObject"/>. /// Invoked for this <see cref="DrawableHitObject"/> to revert any values previously taken on from the currently-applied <see cref="HitObject"/>.
/// </summary> /// </summary>
/// <param name="hitObject">The currently-applied <see cref="HitObject"/>.</param> protected virtual void OnFree()
protected virtual void OnFree(HitObject hitObject)
{ {
} }
@ -519,7 +530,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin, allowFallback);
updateComboColour(); UpdateComboColour();
ApplySkin(skin, allowFallback); ApplySkin(skin, allowFallback);
@ -527,13 +538,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
updateState(State.Value, true); updateState(State.Value, true);
} }
private void updateComboColour() protected void UpdateComboColour()
{ {
if (!(HitObject is IHasComboInformation)) return; if (!(HitObject is IHasComboInformation combo)) return;
var comboColours = CurrentSkin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value; var comboColours = CurrentSkin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>();
AccentColour.Value = combo.GetComboColour(comboColours);
AccentColour.Value = GetComboColour(comboColours);
} }
/// <summary> /// <summary>
@ -544,6 +554,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// This will only be called if the <see cref="HitObject"/> implements <see cref="IHasComboInformation"/>. /// This will only be called if the <see cref="HitObject"/> implements <see cref="IHasComboInformation"/>.
/// </remarks> /// </remarks>
/// <param name="comboColours">A list of combo colours provided by the beatmap or skin. Can be null if not available.</param> /// <param name="comboColours">A list of combo colours provided by the beatmap or skin. Can be null if not available.</param>
[Obsolete("Unused. Implement IHasComboInformation and IHasComboInformation.GetComboColour() on the HitObject model instead.")] // Can be removed 20210527
protected virtual Color4 GetComboColour(IReadOnlyList<Color4> comboColours) protected virtual Color4 GetComboColour(IReadOnlyList<Color4> comboColours)
{ {
if (!(HitObject is IHasComboInformation combo)) if (!(HitObject is IHasComboInformation combo))

View File

@ -1,7 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Objects.Types namespace osu.Game.Rulesets.Objects.Types
{ {
@ -35,5 +38,13 @@ namespace osu.Game.Rulesets.Objects.Types
/// Whether this is the last object in the current combo. /// Whether this is the last object in the current combo.
/// </summary> /// </summary>
bool LastInCombo { get; set; } bool LastInCombo { get; set; }
/// <summary>
/// Retrieves the colour of the combo described by this <see cref="IHasComboInformation"/> object from a set of possible combo colours.
/// Defaults to using <see cref="ComboIndex"/> to decide the colour.
/// </summary>
/// <param name="comboColours">A list of possible combo colours provided by the beatmap or skin.</param>
/// <returns>The colour of the combo described by this <see cref="IHasComboInformation"/> object.</returns>
Color4 GetComboColour([NotNull] IReadOnlyList<Color4> comboColours) => comboColours.Count > 0 ? comboColours[ComboIndex % comboColours.Count] : Color4.White;
} }
} }

View File

@ -114,6 +114,7 @@ namespace osu.Game.Rulesets.UI
bindStartTime(drawable); bindStartTime(drawable);
AddInternal(drawableMap[entry] = drawable, false); AddInternal(drawableMap[entry] = drawable, false);
OnAdd(drawable);
HitObjectUsageBegan?.Invoke(entry.HitObject); HitObjectUsageBegan?.Invoke(entry.HitObject);
} }
@ -129,6 +130,7 @@ namespace osu.Game.Rulesets.UI
drawableMap.Remove(entry); drawableMap.Remove(entry);
OnRemove(drawable);
unbindStartTime(drawable); unbindStartTime(drawable);
RemoveInternal(drawable); RemoveInternal(drawable);
@ -147,10 +149,12 @@ namespace osu.Game.Rulesets.UI
hitObject.OnRevertResult += onRevertResult; hitObject.OnRevertResult += onRevertResult;
AddInternal(hitObject); AddInternal(hitObject);
OnAdd(hitObject);
} }
public virtual bool Remove(DrawableHitObject hitObject) public virtual bool Remove(DrawableHitObject hitObject)
{ {
OnRemove(hitObject);
if (!RemoveInternal(hitObject)) if (!RemoveInternal(hitObject))
return false; return false;
@ -178,6 +182,26 @@ namespace osu.Game.Rulesets.UI
#endregion #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(bool disposeChildren = true) public virtual void Clear(bool disposeChildren = true)
{ {
lifetimeManager.ClearEntries(); lifetimeManager.ClearEntries();

View File

@ -135,10 +135,8 @@ namespace osu.Game.Rulesets.UI
/// <param name="h">The DrawableHitObject to add.</param> /// <param name="h">The DrawableHitObject to add.</param>
public virtual void Add(DrawableHitObject h) public virtual void Add(DrawableHitObject h)
{ {
if (h.IsInitialized) if (!h.IsInitialized)
throw new InvalidOperationException($"{nameof(Add)} doesn't support {nameof(DrawableHitObject)} reuse. Use pooling instead."); onNewDrawableHitObject(h);
onNewDrawableHitObject(h);
HitObjectContainer.Add(h); HitObjectContainer.Add(h);
OnHitObjectAdded(h.HitObject); OnHitObjectAdded(h.HitObject);

View File

@ -2,13 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Framework.Threading;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK; using osuTK;
@ -19,7 +16,16 @@ namespace osu.Game.Rulesets.UI.Scrolling
{ {
private readonly IBindable<double> timeRange = new BindableDouble(); private readonly IBindable<double> timeRange = new BindableDouble();
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly Dictionary<DrawableHitObject, InitialState> hitObjectInitialStateCache = new Dictionary<DrawableHitObject, InitialState>();
/// <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.
/// </summary>
private readonly HashSet<DrawableHitObject> layoutComputed = new HashSet<DrawableHitObject>();
[Resolved] [Resolved]
private IScrollingInfo scrollingInfo { get; set; } private IScrollingInfo scrollingInfo { get; set; }
@ -27,10 +33,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Responds to changes in the layout. When the layout changes, all hit object states must be recomputed. // Responds to changes in the layout. When the layout changes, all hit object states must be recomputed.
private readonly LayoutValue layoutCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo); private readonly LayoutValue layoutCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
// A combined cache across all hit object states to reduce per-update iterations.
// When invalidated, one or more (but not necessarily all) hitobject states must be re-validated.
private readonly Cached combinedObjCache = new Cached();
public ScrollingHitObjectContainer() public ScrollingHitObjectContainer()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -48,37 +50,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
timeRange.ValueChanged += _ => layoutCache.Invalidate(); timeRange.ValueChanged += _ => layoutCache.Invalidate();
} }
public override void Add(DrawableHitObject hitObject)
{
combinedObjCache.Invalidate();
hitObject.DefaultsApplied += onDefaultsApplied;
base.Add(hitObject);
}
public override bool Remove(DrawableHitObject hitObject)
{
var result = base.Remove(hitObject);
if (result)
{
combinedObjCache.Invalidate();
hitObjectInitialStateCache.Remove(hitObject);
hitObject.DefaultsApplied -= onDefaultsApplied;
}
return result;
}
public override void Clear(bool disposeChildren = true) public override void Clear(bool disposeChildren = true)
{ {
foreach (var h in Objects)
h.DefaultsApplied -= onDefaultsApplied;
base.Clear(disposeChildren); base.Clear(disposeChildren);
combinedObjCache.Invalidate(); toComputeLifetime.Clear();
hitObjectInitialStateCache.Clear(); layoutComputed.Clear();
} }
/// <summary> /// <summary>
@ -173,15 +150,40 @@ namespace osu.Game.Rulesets.UI.Scrolling
} }
} }
private void onDefaultsApplied(DrawableHitObject drawableObject) protected override void OnAdd(DrawableHitObject drawableHitObject) => onAddRecursive(drawableHitObject);
protected override void OnRemove(DrawableHitObject drawableHitObject) => onRemoveRecursive(drawableHitObject);
private void onAddRecursive(DrawableHitObject hitObject)
{ {
// The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame). invalidateHitObject(hitObject);
// In such a case, combinedObjCache will take care of updating the hitobject.
if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var state)) hitObject.DefaultsApplied += invalidateHitObject;
{
combinedObjCache.Invalidate(); foreach (var nested in hitObject.NestedHitObjects)
state.Cache.Invalidate(); onAddRecursive(nested);
} }
private void onRemoveRecursive(DrawableHitObject hitObject)
{
toComputeLifetime.Remove(hitObject);
layoutComputed.Remove(hitObject);
hitObject.DefaultsApplied -= invalidateHitObject;
foreach (var nested in hitObject.NestedHitObjects)
onRemoveRecursive(nested);
}
/// <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);
layoutComputed.Remove(hitObject);
} }
private float scrollLength; private float scrollLength;
@ -192,17 +194,18 @@ namespace osu.Game.Rulesets.UI.Scrolling
if (!layoutCache.IsValid) if (!layoutCache.IsValid)
{ {
foreach (var state in hitObjectInitialStateCache.Values) toComputeLifetime.Clear();
state.Cache.Invalidate();
combinedObjCache.Invalidate(); foreach (var hitObject in Objects)
{
if (hitObject.HitObject != null)
toComputeLifetime.Add(hitObject);
}
layoutComputed.Clear();
scrollingInfo.Algorithm.Reset(); scrollingInfo.Algorithm.Reset();
layoutCache.Validate();
}
if (!combinedObjCache.IsValid)
{
switch (direction.Value) switch (direction.Value)
{ {
case ScrollingDirection.Up: case ScrollingDirection.Up:
@ -215,32 +218,24 @@ namespace osu.Game.Rulesets.UI.Scrolling
break; break;
} }
foreach (var obj in Objects) layoutCache.Validate();
{
if (!hitObjectInitialStateCache.TryGetValue(obj, out var state))
state = hitObjectInitialStateCache[obj] = new InitialState(new Cached());
if (state.Cache.IsValid)
continue;
state.ScheduledComputation?.Cancel();
state.ScheduledComputation = computeInitialStateRecursive(obj);
computeLifetimeStartRecursive(obj);
state.Cache.Validate();
}
combinedObjCache.Validate();
} }
}
private void computeLifetimeStartRecursive(DrawableHitObject hitObject) foreach (var hitObject in toComputeLifetime)
{ hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
foreach (var obj in hitObject.NestedHitObjects) toComputeLifetime.Clear();
computeLifetimeStartRecursive(obj);
// only AliveObjects need to be considered for layout (reduces overhead in the case of scroll speed changes).
foreach (var obj in AliveObjects)
{
if (layoutComputed.Contains(obj))
continue;
updateLayoutRecursive(obj);
layoutComputed.Add(obj);
}
} }
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject) private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
@ -271,7 +266,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength); return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
} }
private ScheduledDelegate computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => private void updateLayoutRecursive(DrawableHitObject hitObject)
{ {
if (hitObject.HitObject is IHasDuration e) if (hitObject.HitObject is IHasDuration e)
{ {
@ -291,12 +286,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
foreach (var obj in hitObject.NestedHitObjects) foreach (var obj in hitObject.NestedHitObjects)
{ {
computeInitialStateRecursive(obj); updateLayoutRecursive(obj);
// Nested hitobjects don't need to scroll, but they do need accurate positions // Nested hitobjects don't need to scroll, but they do need accurate positions
updatePosition(obj, hitObject.HitObject.StartTime); updatePosition(obj, hitObject.HitObject.StartTime);
} }
}); }
protected override void UpdateAfterChildrenLife() protected override void UpdateAfterChildrenLife()
{ {
@ -328,19 +323,5 @@ namespace osu.Game.Rulesets.UI.Scrolling
break; break;
} }
} }
private class InitialState
{
[NotNull]
public readonly Cached Cache;
[CanBeNull]
public ScheduledDelegate ScheduledComputation;
public InitialState(Cached cache)
{
Cache = cache;
}
}
} }
} }

View File

@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
{ {
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>(); protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
public new ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)base.HitObjectContainer;
[Resolved] [Resolved]
protected IScrollingInfo ScrollingInfo { get; private set; } protected IScrollingInfo ScrollingInfo { get; private set; }
@ -27,14 +29,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// <summary> /// <summary>
/// Given a position in screen space, return the time within this column. /// Given a position in screen space, return the time within this column.
/// </summary> /// </summary>
public virtual double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) => public virtual double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) => HitObjectContainer.TimeAtScreenSpacePosition(screenSpacePosition);
((ScrollingHitObjectContainer)HitObjectContainer).TimeAtScreenSpacePosition(screenSpacePosition);
/// <summary> /// <summary>
/// Given a time, return the screen space position within this column. /// Given a time, return the screen space position within this column.
/// </summary> /// </summary>
public virtual Vector2 ScreenSpacePositionAtTime(double time) public virtual Vector2 ScreenSpacePositionAtTime(double time) => HitObjectContainer.ScreenSpacePositionAtTime(time);
=> ((ScrollingHitObjectContainer)HitObjectContainer).ScreenSpacePositionAtTime(time);
protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(); protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer();
} }

View File

@ -496,10 +496,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime; double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime;
foreach (HitObject obj in Beatmap.SelectedHitObjects) foreach (HitObject obj in Beatmap.SelectedHitObjects)
{
obj.StartTime += offset; obj.StartTime += offset;
Beatmap.Update(obj);
}
} }
return true; return true;

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -19,8 +18,8 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -28,32 +27,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public class TimelineHitObjectBlueprint : SelectionBlueprint public class TimelineHitObjectBlueprint : SelectionBlueprint
{ {
private readonly Circle circle; private const float thickness = 5;
private const float shadow_radius = 5;
private const float circle_size = 24;
public Action<DragEvent> OnDragHandled;
[UsedImplicitly] [UsedImplicitly]
private readonly Bindable<double> startTime; private readonly Bindable<double> startTime;
public Action<DragEvent> OnDragHandled; private Bindable<int> indexInCurrentComboBindable;
private Bindable<int> comboIndexBindable;
private readonly Circle circle;
private readonly DragBar dragBar; private readonly DragBar dragBar;
private readonly List<Container> shadowComponents = new List<Container>(); private readonly List<Container> shadowComponents = new List<Container>();
private DrawableHitObject drawableHitObject;
private Bindable<Color4> comboColour;
private readonly Container mainComponents; private readonly Container mainComponents;
private readonly OsuSpriteText comboIndexText; private readonly OsuSpriteText comboIndexText;
private Bindable<int> comboIndex; [Resolved]
private ISkinSource skin { get; set; }
private const float thickness = 5;
private const float shadow_radius = 5;
private const float circle_size = 24;
public TimelineHitObjectBlueprint(HitObject hitObject) public TimelineHitObjectBlueprint(HitObject hitObject)
: base(hitObject) : base(hitObject)
@ -152,46 +145,42 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
updateShadows(); updateShadows();
} }
[BackgroundDependencyLoader(true)]
private void load(HitObjectComposer composer)
{
if (composer != null)
{
// best effort to get the drawable representation for grabbing colour and what not.
drawableHitObject = composer.HitObjects.FirstOrDefault(d => d.HitObject == HitObject);
}
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
if (HitObject is IHasComboInformation comboInfo) if (HitObject is IHasComboInformation comboInfo)
{ {
comboIndex = comboInfo.IndexInCurrentComboBindable.GetBoundCopy(); indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy();
comboIndex.BindValueChanged(combo => indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true);
{
comboIndexText.Text = (combo.NewValue + 1).ToString(); comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy();
}, true); comboIndexBindable.BindValueChanged(_ => updateComboColour(), true);
skin.SourceChanged += updateComboColour;
} }
}
if (drawableHitObject != null) private void updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString();
{
comboColour = drawableHitObject.AccentColour.GetBoundCopy();
comboColour.BindValueChanged(colour =>
{
if (HitObject is IHasDuration)
mainComponents.Colour = ColourInfo.GradientHorizontal(drawableHitObject.AccentColour.Value, Color4.White);
else
mainComponents.Colour = drawableHitObject.AccentColour.Value;
var col = mainComponents.Colour.TopLeft.Linear; private void updateComboColour()
float brightness = col.R + col.G + col.B; {
if (!(HitObject is IHasComboInformation combo))
return;
// decide the combo index colour based on brightness? var comboColours = skin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>();
comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White; var comboColour = combo.GetComboColour(comboColours);
}, true);
} if (HitObject is IHasDuration)
mainComponents.Colour = ColourInfo.GradientHorizontal(comboColour, Color4.White);
else
mainComponents.Colour = comboColour;
var col = mainComponents.Colour.TopLeft.Linear;
float brightness = col.R + col.G + col.B;
// decide the combo index colour based on brightness?
comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White;
} }
protected override void Update() protected override void Update()

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -43,6 +44,21 @@ namespace osu.Game.Screens.Edit.Compose
if (ruleset == null || composer == null) if (ruleset == null || composer == null)
return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer"); return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer");
return wrapSkinnableContent(composer);
}
protected override Drawable CreateTimelineContent()
{
if (ruleset == null || composer == null)
return base.CreateTimelineContent();
return wrapSkinnableContent(new TimelineBlueprintContainer(composer));
}
private Drawable wrapSkinnableContent(Drawable content)
{
Debug.Assert(ruleset != null);
var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
// the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
@ -51,9 +67,7 @@ namespace osu.Game.Screens.Edit.Compose
// load the skinning hierarchy first. // load the skinning hierarchy first.
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer)); return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(content));
} }
protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineBlueprintContainer(composer);
} }
} }

View File

@ -76,7 +76,7 @@ namespace osu.Game.Screens.Edit
var newState = stream.ToArray(); var newState = stream.ToArray();
// if the previous state is binary equal we don't need to push a new one, unless this is the initial state. // if the previous state is binary equal we don't need to push a new one, unless this is the initial state.
if (savedStates.Count > 0 && newState.SequenceEqual(savedStates.Last())) return; if (savedStates.Count > 0 && newState.SequenceEqual(savedStates[currentState])) return;
if (currentState < savedStates.Count - 1) if (currentState < savedStates.Count - 1)
savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1);

View File

@ -339,7 +339,11 @@ namespace osu.Game.Screens.Play
AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded },
IsCounting = false IsCounting = false
}, },
RequestSeek = GameplayClockContainer.Seek, RequestSeek = time =>
{
GameplayClockContainer.Seek(time);
GameplayClockContainer.Start();
},
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Origin = Anchor.Centre
}, },

View File

@ -1,12 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Input.Bindings;
using osu.Game.Input.Bindings;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
public class ReplayPlayer : Player public class ReplayPlayer : Player, IKeyBindingHandler<GlobalAction>
{ {
protected readonly Score Score; protected readonly Score Score;
@ -35,5 +37,24 @@ namespace osu.Game.Screens.Play
return Score.ScoreInfo; return Score.ScoreInfo;
} }
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.TogglePauseReplay:
if (GameplayClockContainer.IsPaused.Value)
GameplayClockContainer.Start();
else
GameplayClockContainer.Stop();
return true;
}
return false;
}
public void OnReleased(GlobalAction action)
{
}
} }
} }

View File

@ -133,6 +133,9 @@ namespace osu.Game.Screens.Play
switch (action) switch (action)
{ {
case GlobalAction.SkipCutscene: case GlobalAction.SkipCutscene:
if (!button.Enabled.Value)
return false;
button.Click(); button.Click();
return true; return true;
} }

View File

@ -91,7 +91,7 @@ namespace osu.Game.Screens.Select
/// </summary> /// </summary>
public bool BeatmapSetsLoaded { get; private set; } public bool BeatmapSetsLoaded { get; private set; }
private readonly CarouselScrollContainer scroll; protected readonly CarouselScrollContainer Scroll;
private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Children.OfType<CarouselBeatmapSet>(); private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Children.OfType<CarouselBeatmapSet>();
@ -112,9 +112,9 @@ namespace osu.Game.Screens.Select
if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
selectedBeatmapSet = null; selectedBeatmapSet = null;
ScrollableContent.Clear(false); Scroll.Clear(false);
itemsCache.Invalidate(); itemsCache.Invalidate();
scrollPositionCache.Invalidate(); ScrollToSelected();
// apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false). // apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false).
FlushPendingFilterOperations(); FlushPendingFilterOperations();
@ -130,9 +130,7 @@ namespace osu.Game.Screens.Select
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>(); private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
private readonly Cached itemsCache = new Cached(); private readonly Cached itemsCache = new Cached();
private readonly Cached scrollPositionCache = new Cached(); private PendingScrollOperation pendingScrollOperation = PendingScrollOperation.None;
protected readonly Container<DrawableCarouselItem> ScrollableContent;
public Bindable<bool> RightClickScrollingEnabled = new Bindable<bool>(); public Bindable<bool> RightClickScrollingEnabled = new Bindable<bool>();
@ -155,17 +153,12 @@ namespace osu.Game.Screens.Select
InternalChild = new OsuContextMenuContainer InternalChild = new OsuContextMenuContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = scroll = new CarouselScrollContainer Children = new Drawable[]
{ {
Masking = false, setPool,
RelativeSizeAxes = Axes.Both, Scroll = new CarouselScrollContainer
Children = new Drawable[]
{ {
setPool, RelativeSizeAxes = Axes.Both,
ScrollableContent = new Container<DrawableCarouselItem>
{
RelativeSizeAxes = Axes.X,
}
} }
} }
}; };
@ -180,7 +173,7 @@ namespace osu.Game.Screens.Select
config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm);
config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled);
RightClickScrollingEnabled.ValueChanged += enabled => scroll.RightMouseScrollbar = enabled.NewValue; RightClickScrollingEnabled.ValueChanged += enabled => Scroll.RightMouseScrollbar = enabled.NewValue;
RightClickScrollingEnabled.TriggerChange(); RightClickScrollingEnabled.TriggerChange();
itemUpdated = beatmaps.ItemUpdated.GetBoundCopy(); itemUpdated = beatmaps.ItemUpdated.GetBoundCopy();
@ -421,12 +414,12 @@ namespace osu.Game.Screens.Select
/// <summary> /// <summary>
/// The position of the lower visible bound with respect to the current scroll position. /// The position of the lower visible bound with respect to the current scroll position.
/// </summary> /// </summary>
private float visibleBottomBound => scroll.Current + DrawHeight + BleedBottom; private float visibleBottomBound => Scroll.Current + DrawHeight + BleedBottom;
/// <summary> /// <summary>
/// The position of the upper visible bound with respect to the current scroll position. /// The position of the upper visible bound with respect to the current scroll position.
/// </summary> /// </summary>
private float visibleUpperBound => scroll.Current - BleedTop; private float visibleUpperBound => Scroll.Current - BleedTop;
public void FlushPendingFilterOperations() public void FlushPendingFilterOperations()
{ {
@ -468,8 +461,8 @@ namespace osu.Game.Screens.Select
root.Filter(activeCriteria); root.Filter(activeCriteria);
itemsCache.Invalidate(); itemsCache.Invalidate();
if (alwaysResetScrollPosition || !scroll.UserScrolling) if (alwaysResetScrollPosition || !Scroll.UserScrolling)
ScrollToSelected(); ScrollToSelected(true);
} }
} }
@ -478,7 +471,12 @@ namespace osu.Game.Screens.Select
/// <summary> /// <summary>
/// Scroll to the current <see cref="SelectedBeatmap"/>. /// Scroll to the current <see cref="SelectedBeatmap"/>.
/// </summary> /// </summary>
public void ScrollToSelected() => scrollPositionCache.Invalidate(); /// <param name="immediate">
/// Whether the scroll position should immediately be shifted to the target, delegating animation to visible panels.
/// This should be true for operations like filtering - where panels are changing visibility state - to avoid large jumps in animation.
/// </param>
public void ScrollToSelected(bool immediate = false) =>
pendingScrollOperation = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard;
#region Key / button selection logic #region Key / button selection logic
@ -488,12 +486,12 @@ namespace osu.Game.Screens.Select
{ {
case Key.Left: case Key.Left:
if (!e.Repeat) if (!e.Repeat)
beginRepeatSelection(() => SelectNext(-1, true), e.Key); beginRepeatSelection(() => SelectNext(-1), e.Key);
return true; return true;
case Key.Right: case Key.Right:
if (!e.Repeat) if (!e.Repeat)
beginRepeatSelection(() => SelectNext(1, true), e.Key); beginRepeatSelection(() => SelectNext(), e.Key);
return true; return true;
} }
@ -580,6 +578,11 @@ namespace osu.Game.Screens.Select
if (revalidateItems) if (revalidateItems)
updateYPositions(); updateYPositions();
// if there is a pending scroll action we apply it without animation and transfer the difference in position to the panels.
// this is intentionally applied before updating the visible range below, to avoid animating new items (sourced from pool) from locations off-screen, as it looks bad.
if (pendingScrollOperation != PendingScrollOperation.None)
updateScrollPosition();
// This data is consumed to find the currently displayable range. // This data is consumed to find the currently displayable range.
// This is the range we want to keep drawables for, and should exceed the visible range slightly to avoid drawable churn. // This is the range we want to keep drawables for, and should exceed the visible range slightly to avoid drawable churn.
var newDisplayRange = getDisplayRange(); var newDisplayRange = getDisplayRange();
@ -594,7 +597,7 @@ namespace osu.Game.Screens.Select
{ {
var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1); var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1);
foreach (var panel in ScrollableContent.Children) foreach (var panel in Scroll.Children)
{ {
if (toDisplay.Remove(panel.Item)) if (toDisplay.Remove(panel.Item))
{ {
@ -620,24 +623,14 @@ namespace osu.Game.Screens.Select
panel.Depth = item.CarouselYPosition; panel.Depth = item.CarouselYPosition;
panel.Y = item.CarouselYPosition; panel.Y = item.CarouselYPosition;
ScrollableContent.Add(panel); Scroll.Add(panel);
} }
} }
} }
// Finally, if the filtered items have changed, animate drawables to their new locations.
// This is common if a selected/collapsed state has changed.
if (revalidateItems)
{
foreach (DrawableCarouselItem panel in ScrollableContent.Children)
{
panel.MoveToY(panel.Item.CarouselYPosition, 800, Easing.OutQuint);
}
}
// Update externally controlled state of currently visible items (e.g. x-offset and opacity). // Update externally controlled state of currently visible items (e.g. x-offset and opacity).
// This is a per-frame update on all drawable panels. // This is a per-frame update on all drawable panels.
foreach (DrawableCarouselItem item in ScrollableContent.Children) foreach (DrawableCarouselItem item in Scroll.Children)
{ {
updateItem(item); updateItem(item);
@ -670,14 +663,6 @@ namespace osu.Game.Screens.Select
return (firstIndex, lastIndex); return (firstIndex, lastIndex);
} }
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
if (!scrollPositionCache.IsValid)
updateScrollPosition();
}
private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem) private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
{ {
if (weakItem.NewValue.TryGetTarget(out var item)) if (weakItem.NewValue.TryGetTarget(out var item))
@ -789,7 +774,8 @@ namespace osu.Game.Screens.Select
} }
currentY += visibleHalfHeight; currentY += visibleHalfHeight;
ScrollableContent.Height = currentY;
Scroll.ScrollContent.Height = currentY;
if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected)) if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected))
{ {
@ -809,12 +795,31 @@ namespace osu.Game.Screens.Select
if (firstScroll) if (firstScroll)
{ {
// reduce movement when first displaying the carousel. // reduce movement when first displaying the carousel.
scroll.ScrollTo(scrollTarget.Value - 200, false); Scroll.ScrollTo(scrollTarget.Value - 200, false);
firstScroll = false; firstScroll = false;
} }
scroll.ScrollTo(scrollTarget.Value); switch (pendingScrollOperation)
scrollPositionCache.Validate(); {
case PendingScrollOperation.Standard:
Scroll.ScrollTo(scrollTarget.Value);
break;
case PendingScrollOperation.Immediate:
// in order to simplify animation logic, rather than using the animated version of ScrollTo,
// we take the difference in scroll height and apply to all visible panels.
// this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer
// to enter clamp-special-case mode where it animates completely differently to normal.
float scrollChange = scrollTarget.Value - Scroll.Current;
Scroll.ScrollTo(scrollTarget.Value, false);
foreach (var i in Scroll.Children)
i.Y += scrollChange;
break;
}
pendingScrollOperation = PendingScrollOperation.None;
} }
} }
@ -844,7 +849,7 @@ namespace osu.Game.Screens.Select
/// <param name="parent">For nested items, the parent of the item to be updated.</param> /// <param name="parent">For nested items, the parent of the item to be updated.</param>
private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null) private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null)
{ {
Vector2 posInScroll = ScrollableContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre); Vector2 posInScroll = Scroll.ScrollContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre);
float itemDrawY = posInScroll.Y - visibleUpperBound; float itemDrawY = posInScroll.Y - visibleUpperBound;
float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight); float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight);
@ -858,6 +863,13 @@ namespace osu.Game.Screens.Select
item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1)); item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1));
} }
private enum PendingScrollOperation
{
None,
Standard,
Immediate,
}
/// <summary> /// <summary>
/// A carousel item strictly used for binary search purposes. /// A carousel item strictly used for binary search purposes.
/// </summary> /// </summary>
@ -889,7 +901,7 @@ namespace osu.Game.Screens.Select
} }
} }
private class CarouselScrollContainer : OsuScrollContainer protected class CarouselScrollContainer : OsuScrollContainer<DrawableCarouselItem>
{ {
private bool rightMouseScrollBlocked; private bool rightMouseScrollBlocked;
@ -898,6 +910,12 @@ namespace osu.Game.Screens.Select
/// </summary> /// </summary>
public bool UserScrolling { get; private set; } public bool UserScrolling { get; private set; }
public CarouselScrollContainer()
{
// size is determined by the carousel itself, due to not all content necessarily being loaded.
ScrollContent.AutoSizeAxes = Axes.None;
}
// ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910) // ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910)
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
{ {

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -60,6 +61,25 @@ namespace osu.Game.Screens.Select.Carousel
viewDetails = beatmapOverlay.FetchAndShowBeatmapSet; viewDetails = beatmapOverlay.FetchAndShowBeatmapSet;
} }
protected override void Update()
{
base.Update();
// position updates should not occur if the item is filtered away.
// this avoids panels flying across the screen only to be eventually off-screen or faded out.
if (!Item.Visible)
return;
float targetY = Item.CarouselYPosition;
if (Precision.AlmostEquals(targetY, Y))
Y = targetY;
else
// algorithm for this is taken from ScrollContainer.
// while it doesn't necessarily need to match 1:1, as we are emulating scroll in some cases this feels most correct.
Y = (float)Interpolation.Lerp(targetY, Y, Math.Exp(-0.01 * Time.Elapsed));
}
protected override void UpdateItem() protected override void UpdateItem()
{ {
base.UpdateItem(); base.UpdateItem();

View File

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

View File

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