diff --git a/osu.Android.props b/osu.Android.props index e95c7e6619..1216b1772f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 517027a9fc..900691ecae 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -216,7 +216,7 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true)); AddStep("catch fruit", () => attemptCatch(new Fruit())); AddAssert("correct hit lighting colour", () => - catcher.ChildrenOfType().First()?.ObjectColour == fruitColour); + catcher.ChildrenOfType().First()?.Entry?.ObjectColour == fruitColour); } [Test] diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs index 140b411c88..7c88090a20 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . 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.Skinning.Default; namespace osu.Game.Rulesets.Catch.Objects.Drawables @@ -9,21 +8,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables /// /// Represents a caught by the catcher. /// - public class CaughtFruit : CaughtObject, IHasFruitState + public class CaughtFruit : CaughtObject { - public Bindable VisualRepresentation { get; } = new Bindable(); - public CaughtFruit() : base(CatchSkinComponents.Fruit, _ => new FruitPiece()) { } - - public override void CopyStateFrom(IHasCatchObjectState objectState) - { - base.CopyStateFrom(objectState); - - var fruitState = (IHasFruitState)objectState; - VisualRepresentation.Value = fruitState.VisualRepresentation.Value; - } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs index 524505d588..d8bce9bb6d 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public PalpableCatchHitObject HitObject { get; private set; } public Bindable AccentColour { get; } = new Bindable(); public Bindable HyperDash { get; } = new Bindable(); + public Bindable IndexInBeatmap { get; } = new Bindable(); public Vector2 DisplaySize => Size * Scale; @@ -51,6 +52,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Rotation = objectState.DisplayRotation; AccentColour.Value = objectState.AccentColour.Value; HyperDash.Value = objectState.HyperDash.Value; + IndexInBeatmap.Value = objectState.IndexInBeatmap.Value; } protected override void FreeAfterUse() diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 0b89c46480..0af7ee6c30 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -3,17 +3,14 @@ using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableFruit : DrawablePalpableCatchHitObject, IHasFruitState + public class DrawableFruit : DrawablePalpableCatchHitObject { - public Bindable VisualRepresentation { get; } = new Bindable(); - public DrawableFruit() : this(null) { @@ -27,11 +24,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - IndexInBeatmap.BindValueChanged(change => - { - VisualRepresentation.Value = (FruitVisualRepresentation)(change.NewValue % 4); - }, true); - ScalingContainer.Child = new SkinnableDrawable( new CatchSkinComponent(CatchSkinComponents.Fruit), _ => new FruitPiece()); @@ -44,12 +36,4 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40); } } - - public enum FruitVisualRepresentation - { - Pear, - Grape, - Pineapple, - Raspberry, - } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs index 81b61f0959..be0ee2821e 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs @@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Bindable HyperDash { get; } + Bindable IndexInBeatmap { get; } + Vector2 DisplaySize { get; } float DisplayRotation { get; } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasFruitState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasFruitState.cs deleted file mode 100644 index 2d4de543c3..0000000000 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasFruitState.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Bindables; - -namespace osu.Game.Rulesets.Catch.Objects.Drawables -{ - /// - /// Provides a visual state of a . - /// - public interface IHasFruitState : IHasCatchObjectState - { - Bindable VisualRepresentation { get; } - } -} diff --git a/osu.Game.Rulesets.Catch/Objects/Fruit.cs b/osu.Game.Rulesets.Catch/Objects/Fruit.cs index 43486796ad..4818fe2cad 100644 --- a/osu.Game.Rulesets.Catch/Objects/Fruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Fruit.cs @@ -9,5 +9,7 @@ namespace osu.Game.Rulesets.Catch.Objects public class Fruit : PalpableCatchHitObject { public override Judgement CreateJudgement() => new CatchJudgement(); + + public static FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4); } } diff --git a/osu.Game.Rulesets.Catch/Objects/FruitVisualRepresentation.cs b/osu.Game.Rulesets.Catch/Objects/FruitVisualRepresentation.cs new file mode 100644 index 0000000000..7ec7050245 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/FruitVisualRepresentation.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Catch.Objects +{ + public enum FruitVisualRepresentation + { + Pear, + Grape, + Pineapple, + Raspberry, + } +} diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs index 51c06c8e37..2db3bae034 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs @@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default { public readonly Bindable AccentColour = new Bindable(); public readonly Bindable HyperDash = new Bindable(); + public readonly Bindable IndexInBeatmap = new Bindable(); [Resolved] protected IHasCatchObjectState ObjectState { get; private set; } @@ -37,6 +38,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default AccentColour.BindTo(ObjectState.AccentColour); HyperDash.BindTo(ObjectState.HyperDash); + IndexInBeatmap.BindTo(ObjectState.IndexInBeatmap); HyperDash.BindValueChanged(hyper => { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs index 49f128c960..cfe0df0c97 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs @@ -3,7 +3,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.Objects; namespace osu.Game.Rulesets.Catch.Skinning.Default { @@ -39,8 +39,10 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default { base.LoadComplete(); - var fruitState = (IHasFruitState)ObjectState; - VisualRepresentation.BindTo(fruitState.VisualRepresentation); + IndexInBeatmap.BindValueChanged(index => + { + VisualRepresentation.Value = Fruit.GetVisualRepresentation(index.NewValue); + }, true); } } } diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs index 88e0b5133a..f097361d2a 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.Objects; using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Default diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyBananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs similarity index 91% rename from osu.Game.Rulesets.Catch/Skinning/LegacyBananaPiece.cs rename to osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs index f80e50c8c0..5bd5b0d4bb 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyBananaPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs @@ -3,7 +3,7 @@ using osu.Framework.Graphics.Textures; -namespace osu.Game.Rulesets.Catch.Skinning +namespace osu.Game.Rulesets.Catch.Skinning.Legacy { public class LegacyBananaPiece : LegacyCatchHitObjectPiece { diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchHitObjectPiece.cs similarity index 94% rename from osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs rename to osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchHitObjectPiece.cs index 4b1f5a4724..f78724615a 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchHitObjectPiece.cs @@ -13,12 +13,13 @@ using osu.Game.Skinning; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Catch.Skinning +namespace osu.Game.Rulesets.Catch.Skinning.Legacy { public abstract class LegacyCatchHitObjectPiece : PoolableDrawable { public readonly Bindable AccentColour = new Bindable(); public readonly Bindable HyperDash = new Bindable(); + public readonly Bindable IndexInBeatmap = new Bindable(); private readonly Sprite colouredSprite; private readonly Sprite overlaySprite; @@ -64,6 +65,7 @@ namespace osu.Game.Rulesets.Catch.Skinning AccentColour.BindTo(ObjectState.AccentColour); HyperDash.BindTo(ObjectState.HyperDash); + IndexInBeatmap.BindTo(ObjectState.IndexInBeatmap); hyperSprite.Colour = Skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value ?? Skin.GetConfig(CatchSkinColour.HyperDash)?.Value ?? diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs similarity index 93% rename from osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.cs rename to osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs index 8f4331d2a3..2c5cbe1e41 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics.Textures; using osuTK; -namespace osu.Game.Rulesets.Catch.Skinning +namespace osu.Game.Rulesets.Catch.Skinning.Legacy { public class LegacyDropletPiece : LegacyCatchHitObjectPiece { diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs index 969cc38e5b..f002bab219 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs @@ -1,23 +1,20 @@ // Copyright (c) ppy Pty Ltd . 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.Drawables; +using osu.Game.Rulesets.Catch.Objects; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { internal class LegacyFruitPiece : LegacyCatchHitObjectPiece { - public readonly Bindable VisualRepresentation = new Bindable(); - protected override void LoadComplete() { base.LoadComplete(); - var fruitState = (IHasFruitState)ObjectState; - VisualRepresentation.BindTo(fruitState.VisualRepresentation); - - VisualRepresentation.BindValueChanged(visual => setTexture(visual.NewValue), true); + IndexInBeatmap.BindValueChanged(index => + { + setTexture(Fruit.GetVisualRepresentation(index.NewValue)); + }, true); } private void setTexture(FruitVisualRepresentation visualRepresentation) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 0d6a577d1e..1c29e8b20c 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -126,8 +126,7 @@ namespace osu.Game.Rulesets.Catch.UI private float hyperDashTargetPosition; private Bindable hitLighting; - private readonly DrawablePool hitExplosionPool; - private readonly Container hitExplosionContainer; + private readonly HitExplosionContainer hitExplosionContainer; private readonly DrawablePool caughtFruitPool; private readonly DrawablePool caughtBananaPool; @@ -148,7 +147,6 @@ namespace osu.Game.Rulesets.Catch.UI InternalChildren = new Drawable[] { - hitExplosionPool = new DrawablePool(10), caughtFruitPool = new DrawablePool(50), caughtBananaPool = new DrawablePool(100), // less capacity is needed compared to fruit because droplet is not stacked @@ -173,7 +171,7 @@ namespace osu.Game.Rulesets.Catch.UI Anchor = Anchor.TopCentre, Alpha = 0, }, - hitExplosionContainer = new Container + hitExplosionContainer = new HitExplosionContainer { Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, @@ -297,7 +295,6 @@ namespace osu.Game.Rulesets.Catch.UI caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject); - hitExplosionContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); } /// @@ -508,15 +505,8 @@ namespace osu.Game.Rulesets.Catch.UI return position; } - private void addLighting(CatchHitObject hitObject, float x, Color4 colour) - { - HitExplosion hitExplosion = hitExplosionPool.Get(); - hitExplosion.HitObject = hitObject; - hitExplosion.X = x; - hitExplosion.Scale = new Vector2(hitObject.Scale); - hitExplosion.ObjectColour = colour; - hitExplosionContainer.Add(hitExplosion); - } + private void addLighting(CatchHitObject hitObject, float x, Color4 colour) => + hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, x, hitObject.Scale, colour, hitObject.RandomSeed)); private CaughtObject getCaughtObject(PalpableCatchHitObject source) { diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs index 26627422e1..d9ab428231 100644 --- a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs @@ -5,31 +5,16 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Pooling; using osu.Framework.Utils; -using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Objects.Pooling; +using osu.Game.Utils; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.UI { - public class HitExplosion : PoolableDrawable + public class HitExplosion : PoolableDrawableWithLifetime { - private Color4 objectColour; - public CatchHitObject HitObject; - - public Color4 ObjectColour - { - get => objectColour; - set - { - if (objectColour == value) return; - - objectColour = value; - onColourChanged(); - } - } - private readonly CircularContainer largeFaint; private readonly CircularContainer smallFaint; private readonly CircularContainer directionalGlow1; @@ -83,9 +68,19 @@ namespace osu.Game.Rulesets.Catch.UI }; } - protected override void PrepareForUse() + protected override void OnApply(HitExplosionEntry entry) { - base.PrepareForUse(); + X = entry.Position; + Scale = new Vector2(entry.Scale); + setColour(entry.ObjectColour); + + using (BeginAbsoluteSequence(entry.LifetimeStart)) + applyTransforms(entry.RNGSeed); + } + + private void applyTransforms(int randomSeed) + { + ClearTransforms(true); const double duration = 400; @@ -96,14 +91,13 @@ namespace osu.Game.Rulesets.Catch.UI .FadeOut(duration * 2); const float angle_variangle = 15; // should be less than 45 - directionalGlow1.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle); - directionalGlow2.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle); + directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4); + directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5); - this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out); - Expire(true); + this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out).Expire(); } - private void onColourChanged() + private void setColour(Color4 objectColour) { const float roundness = 100; diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs b/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs new file mode 100644 index 0000000000..094d88243a --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Objects.Pooling; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class HitExplosionContainer : PooledDrawableWithLifetimeContainer + { + protected override bool RemoveRewoundEntry => true; + + private readonly DrawablePool pool; + + public HitExplosionContainer() + { + AddInternal(pool = new DrawablePool(10)); + } + + protected override HitExplosion GetDrawable(HitExplosionEntry entry) => pool.Get(d => d.Apply(entry)); + } +} diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs b/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs new file mode 100644 index 0000000000..b142962a8a --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Performance; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class HitExplosionEntry : LifetimeEntry + { + public readonly float Position; + public readonly float Scale; + public readonly Color4 ObjectColour; + public readonly int RNGSeed; + + public HitExplosionEntry(double startTime, float position, float scale, Color4 objectColour, int rngSeed) + { + LifetimeStart = startTime; + Position = position; + Scale = scale; + ObjectColour = objectColour; + RNGSeed = rngSeed; + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleKiai.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleKiai.cs new file mode 100644 index 0000000000..2bce8fa7f2 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleKiai.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class TestSceneHitCircleKiai : TestSceneHitCircle + { + [SetUp] + public void SetUp() => Schedule(() => + { + var controlPointInfo = new ControlPointInfo(); + + controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 }); + controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true }); + + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + ControlPointInfo = controlPointInfo + }); + + // track needs to be playing for BeatSyncedContainer to work. + Beatmap.Value.Track.Start(); + }); + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index cda4715280..001ea6c4ad 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Pooling; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections @@ -12,34 +13,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections /// /// Visualises the s between two s. /// - public class FollowPointConnection : PoolableDrawable + public class FollowPointConnection : PoolableDrawableWithLifetime { // Todo: These shouldn't be constants public const int SPACING = 32; public const double PREEMPT = 800; - public FollowPointLifetimeEntry Entry; public DrawablePool Pool; - protected override void PrepareForUse() + protected override void OnApply(FollowPointLifetimeEntry entry) { - base.PrepareForUse(); - - Entry.Invalidated += onEntryInvalidated; + base.OnApply(entry); + entry.Invalidated += onEntryInvalidated; refreshPoints(); } - protected override void FreeAfterUse() + protected override void OnFree(FollowPointLifetimeEntry entry) { - base.FreeAfterUse(); - - Entry.Invalidated -= onEntryInvalidated; + base.OnFree(entry); + entry.Invalidated -= onEntryInvalidated; // Return points to the pool. ClearInternal(false); - - Entry = null; } private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints); @@ -48,8 +44,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { ClearInternal(false); - OsuHitObject start = Entry.Start; - OsuHitObject end = Entry.End; + var entry = Entry; + if (entry?.End == null) return; + + OsuHitObject start = entry.Start; + OsuHitObject end = entry.End; double startTime = start.GetEndTime(); @@ -87,14 +86,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections fp.FadeIn(end.TimeFadeIn); fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out); fp.MoveTo(pointEndPosition, end.TimeFadeIn, Easing.Out); - fp.Delay(fadeOutTime - fadeInTime).FadeOut(end.TimeFadeIn); + fp.Delay(fadeOutTime - fadeInTime).FadeOut(end.TimeFadeIn).Expire(); - finalTransformEndTime = fadeOutTime + end.TimeFadeIn; + finalTransformEndTime = fp.LifetimeEnd; } } - // todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed. - Entry.LifetimeEnd = finalTransformEndTime; + entry.LifetimeEnd = finalTransformEndTime; } /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs index a167cb2f0f..82bca0a4e2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using osu.Framework.Bindables; using osu.Framework.Graphics.Performance; @@ -11,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { public class FollowPointLifetimeEntry : LifetimeEntry { - public event Action Invalidated; + public event Action? Invalidated; public readonly OsuHitObject Start; public FollowPointLifetimeEntry(OsuHitObject start) @@ -22,9 +24,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections bindEvents(); } - private OsuHitObject end; + private OsuHitObject? end; - public OsuHitObject End + public OsuHitObject? End { get => end; set @@ -56,11 +58,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections public void UnbindEvents() { - if (Start != null) - { - Start.DefaultsApplied -= onDefaultsApplied; - Start.PositionBindable.ValueChanged -= onPositionChanged; - } + Start.DefaultsApplied -= onDefaultsApplied; + Start.PositionBindable.ValueChanged -= onPositionChanged; if (End != null) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 3e85e528e8..21e6619444 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -6,43 +6,32 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { /// /// Visualises connections between s. /// - public class FollowPointRenderer : CompositeDrawable + public class FollowPointRenderer : PooledDrawableWithLifetimeContainer { - public override bool RemoveCompletedTransforms => false; - - public IReadOnlyList Entries => lifetimeEntries; + public new IReadOnlyList Entries => lifetimeEntries; private DrawablePool connectionPool; private DrawablePool pointPool; private readonly List lifetimeEntries = new List(); - private readonly Dictionary connectionsInUse = new Dictionary(); private readonly Dictionary startTimeMap = new Dictionary(); - private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); - - public FollowPointRenderer() - { - lifetimeManager.EntryBecameAlive += onEntryBecameAlive; - lifetimeManager.EntryBecameDead += onEntryBecameDead; - } [BackgroundDependencyLoader] private void load() { InternalChildren = new Drawable[] { - connectionPool = new DrawablePoolNoLifetime(1, 200), - pointPool = new DrawablePoolNoLifetime(50, 1000) + connectionPool = new DrawablePool(1, 200), + pointPool = new DrawablePool(50, 1000) }; } @@ -107,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections previousEntry.End = newEntry.Start; } - lifetimeManager.AddEntry(newEntry); + Add(newEntry); } private void removeEntry(OsuHitObject hitObject) @@ -118,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections entry.UnbindEvents(); lifetimeEntries.RemoveAt(index); - lifetimeManager.RemoveEntry(entry); + Remove(entry); if (index > 0) { @@ -131,30 +120,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections } } - protected override bool CheckChildrenLife() + protected override FollowPointConnection GetDrawable(FollowPointLifetimeEntry entry) { - bool anyAliveChanged = base.CheckChildrenLife(); - anyAliveChanged |= lifetimeManager.Update(Time.Current); - return anyAliveChanged; - } - - private void onEntryBecameAlive(LifetimeEntry entry) - { - var connection = connectionPool.Get(c => - { - c.Entry = (FollowPointLifetimeEntry)entry; - c.Pool = pointPool; - }); - - connectionsInUse[entry] = connection; - - AddInternal(connection); - } - - private void onEntryBecameDead(LifetimeEntry entry) - { - RemoveInternal(connectionsInUse[entry]); - connectionsInUse.Remove(entry); + var connection = connectionPool.Get(); + connection.Pool = pointPool; + connection.Apply(entry); + return connection; } private void onStartTimeChanged(OsuHitObject hitObject) @@ -171,16 +142,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections entry.UnbindEvents(); lifetimeEntries.Clear(); } - - private class DrawablePoolNoLifetime : DrawablePool - where T : PoolableDrawable, new() - { - public override bool RemoveWhenNotAlive => false; - - public DrawablePoolNoLifetime(int initialSize, int? maximumSize = null) - : base(initialSize, maximumSize) - { - } - } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs index ba41ebd445..cb68d4b7a7 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs @@ -42,6 +42,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default Origin = Anchor.Centre, Texture = textures.Get(@"Gameplay/osu/disc"), }, + new KiaiFlash + { + RelativeSizeAxes = Axes.Both, + }, triangles = new TrianglesPiece { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/KiaiFlash.cs b/osu.Game.Rulesets.Osu/Skinning/Default/KiaiFlash.cs new file mode 100644 index 0000000000..d49b1713f6 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Default/KiaiFlash.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Skinning.Default +{ + public class KiaiFlash : BeatSyncedContainer + { + private const double fade_length = 80; + + private const float flash_opacity = 0.25f; + + public KiaiFlash() + { + EarlyActivationMilliseconds = 80; + Blending = BlendingParameters.Additive; + + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + Alpha = 0f, + }; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + if (!effectPoint.KiaiMode) + return; + + Child + .FadeTo(flash_opacity, EarlyActivationMilliseconds, Easing.OutQuint) + .Then() + .FadeOut(timingPoint.BeatLength - fade_length, Easing.OutSine); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/KiaiFlashingSprite.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/KiaiFlashingSprite.cs new file mode 100644 index 0000000000..4a1d69ad41 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/KiaiFlashingSprite.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Rulesets.Osu.Skinning.Legacy +{ + internal class KiaiFlashingSprite : BeatSyncedContainer + { + private readonly Sprite mainSprite; + private readonly Sprite flashingSprite; + + public Texture Texture + { + set + { + mainSprite.Texture = value; + flashingSprite.Texture = value; + } + } + + private const float flash_opacity = 0.3f; + + public KiaiFlashingSprite() + { + AutoSizeAxes = Axes.Both; + + Children = new Drawable[] + { + mainSprite = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + flashingSprite = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + Blending = BlendingParameters.Additive, + } + }; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + if (!effectPoint.KiaiMode) + return; + + flashingSprite + .FadeTo(flash_opacity) + .Then() + .FadeOut(timingPoint.BeatLength * 0.75f); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index ffbeea5e0e..822dad8523 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -32,9 +31,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); } - private Container circleSprites; - private Sprite hitCircleSprite; - private Sprite hitCircleOverlay; + private Container circleSprites; + private Drawable hitCircleSprite; + private Drawable hitCircleOverlay; private SkinnableSpriteText hitCircleText; @@ -72,20 +71,20 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy InternalChildren = new Drawable[] { - circleSprites = new Container + circleSprites = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Children = new[] { - hitCircleSprite = new Sprite + hitCircleSprite = new KiaiFlashingSprite { Texture = baseTexture, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - hitCircleOverlay = new Sprite + hitCircleOverlay = new KiaiFlashingSprite { Texture = overlayTexture, Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs index 9bfb6aa839..263454c78a 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Performance; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; @@ -11,6 +8,11 @@ namespace osu.Game.Rulesets.Taiko.UI { internal class DrumRollHitContainer : ScrollingHitObjectContainer { + // TODO: this usage is buggy. + // Because `LifetimeStart` is set based on scrolling, lifetime is not same as the time when the object is created. + // If the `Update` override is removed, it breaks in an obscure way. + protected override bool RemoveRewoundEntry => true; + protected override void Update() { base.Update(); @@ -23,14 +25,5 @@ namespace osu.Game.Rulesets.Taiko.UI Remove(flyingHit); } } - - protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e) - { - base.OnChildLifetimeBoundaryCrossed(e); - - // ensure all old hits are removed on becoming alive (may miss being in the AliveInternalChildren list above). - if (e.Kind == LifetimeBoundaryKind.Start && e.Direction == LifetimeBoundaryCrossingDirection.Backward) - Remove((DrawableHitObject)e.Child); - } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs index 2c5443fe08..ab13095af4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs @@ -177,6 +177,7 @@ namespace osu.Game.Tests.Visual.Gameplay public override Container Overlays { get; } public override Container FrameStableComponents { get; } public override IFrameStableClock FrameStableClock { get; } + internal override bool FrameStablePlayback { get; set; } public override IReadOnlyList Mods { get; } public override double GameplayStartTime { get; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index e9894ff469..6eeb3596a8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -76,9 +76,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectator); start(); - sendFrames(); - waitForPlayer(); + + sendFrames(); AddAssert("ensure frames arrived", () => replayHandler.HasFrames); AddUntilStep("wait for frame starvation", () => replayHandler.WaitingForFrame); @@ -116,12 +116,11 @@ namespace osu.Game.Tests.Visual.Gameplay start(); loadSpectatingScreen(); + waitForPlayer(); AddStep("advance frame count", () => nextFrame = 300); sendFrames(); - waitForPlayer(); - AddUntilStep("playing from correct point in time", () => player.ChildrenOfType().First().FrameStableClock.CurrentTime > 30000); } @@ -210,7 +209,7 @@ namespace osu.Game.Tests.Visual.Gameplay private double currentFrameStableTime => player.ChildrenOfType().First().FrameStableClock.CurrentTime; - private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); + private void waitForPlayer() => AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true); private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); diff --git a/osu.Game/Configuration/BackgroundSource.cs b/osu.Game/Configuration/BackgroundSource.cs index 5726e96eb1..18e0603860 100644 --- a/osu.Game/Configuration/BackgroundSource.cs +++ b/osu.Game/Configuration/BackgroundSource.cs @@ -1,11 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.ComponentModel; + namespace osu.Game.Configuration { public enum BackgroundSource { Skin, - Beatmap + Beatmap, + + [Description("Beatmap (with storyboard / video)")] + BeatmapWithStoryboard, } } diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs new file mode 100644 index 0000000000..6a42e83305 --- /dev/null +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Storyboards.Drawables; + +namespace osu.Game.Graphics.Backgrounds +{ + public class BeatmapBackgroundWithStoryboard : BeatmapBackground + { + public BeatmapBackgroundWithStoryboard(WorkingBeatmap beatmap, string fallbackTextureName = "Backgrounds/bg1") + : base(beatmap, fallbackTextureName) + { + } + + [BackgroundDependencyLoader] + private void load() + { + if (!Beatmap.Storyboard.HasDrawable) + return; + + if (Beatmap.Storyboard.ReplacesBackground) + Sprite.Alpha = 0; + + LoadComponentAsync(new AudioContainer + { + RelativeSizeAxes = Axes.Both, + Volume = { Value = 0 }, + Child = new DrawableStoryboard(Beatmap.Storyboard) { Clock = new InterpolatingFramedClock(Beatmap.Track) } + }, AddInternal); + } + } +} diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs index 6facf4e26c..81f30bd406 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs @@ -30,9 +30,12 @@ namespace osu.Game.Graphics.Containers.Markdown break; case ListItemBlock listItemBlock: - var isOrdered = ((ListBlock)listItemBlock.Parent).IsOrdered; - var childContainer = CreateListItem(listItemBlock, level, isOrdered); + bool isOrdered = ((ListBlock)listItemBlock.Parent)?.IsOrdered == true; + + OsuMarkdownListItem childContainer = CreateListItem(listItemBlock, level, isOrdered); + container.Add(childContainer); + foreach (var single in listItemBlock) base.AddMarkdownComponent(single, childContainer.Content, level); break; diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index 2488fd14d0..d2b1e5e523 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -172,6 +172,8 @@ namespace osu.Game.Graphics.Containers private class ScalingBackgroundScreen : BackgroundScreenDefault { + protected override bool AllowStoryboardBackground => false; + public override void OnEntering(IScreen last) { this.FadeInFromZero(4000, Easing.OutQuint); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7935815f38..9c3adba342 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -58,7 +58,7 @@ namespace osu.Game /// /// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects. /// - internal const double GLOBAL_TRACK_VOLUME_ADJUST = 0.5; + internal const double GLOBAL_TRACK_VOLUME_ADJUST = 0.8; public bool UseDevelopmentServer { get; } diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs index acaaa523a2..6f0b433acb 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Wiki.Markdown case ParagraphBlock paragraphBlock: // Check if paragraph only contains an image - if (paragraphBlock.Inline.Count() == 1 && paragraphBlock.Inline.FirstChild is LinkInline { IsImage: true } linkInline) + if (paragraphBlock.Inline?.Count() == 1 && paragraphBlock.Inline.FirstChild is LinkInline { IsImage: true } linkInline) { container.Add(new WikiMarkdownImageBlock(linkInline)); return; diff --git a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs new file mode 100644 index 0000000000..d35933dba8 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs @@ -0,0 +1,163 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Performance; + +namespace osu.Game.Rulesets.Objects.Pooling +{ + /// + /// A container of s dynamically added/removed by model s. + /// When an entry became alive, a drawable corresponding to the entry is obtained (potentially pooled), and added to this container. + /// The drawable is removed when the entry became dead. + /// + /// The type of entries managed by this container. + /// The type of drawables corresponding to the entries. + public abstract class PooledDrawableWithLifetimeContainer : CompositeDrawable + where TEntry : LifetimeEntry + where TDrawable : Drawable + { + /// + /// All entries added to this container, including dead entries. + /// + /// + /// The enumeration order is undefined. + /// + public IEnumerable Entries => allEntries; + + /// + /// All alive entries and drawables corresponding to the entries. + /// + /// + /// The enumeration order is undefined. + /// + public IEnumerable<(TEntry Entry, TDrawable Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value)); + + /// + /// Whether to remove an entry when clock goes backward and crossed its . + /// Used when entries are dynamically added at its to prevent duplicated entries. + /// + protected virtual bool RemoveRewoundEntry => false; + + /// + /// The amount of time prior to the current time within which entries should be considered alive. + /// + internal double PastLifetimeExtension { get; set; } + + /// + /// The amount of time after the current time within which entries should be considered alive. + /// + internal double FutureLifetimeExtension { get; set; } + + private readonly Dictionary aliveDrawableMap = new Dictionary(); + private readonly HashSet allEntries = new HashSet(); + + private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); + + protected PooledDrawableWithLifetimeContainer() + { + lifetimeManager.EntryBecameAlive += entryBecameAlive; + lifetimeManager.EntryBecameDead += entryBecameDead; + lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary; + } + + /// + /// Add a to be managed by this container. + /// + /// + /// The aliveness of the entry is not updated until . + /// + public virtual void Add(TEntry entry) + { + allEntries.Add(entry); + lifetimeManager.AddEntry(entry); + } + + /// + /// Remove a from this container. + /// + /// + /// If the entry was alive, the corresponding drawable is removed. + /// + /// Whether the entry was in this container. + public virtual bool Remove(TEntry entry) + { + if (!lifetimeManager.RemoveEntry(entry)) return false; + + allEntries.Remove(entry); + return true; + } + + /// + /// Initialize new corresponding . + /// + /// The corresponding to the entry. + protected abstract TDrawable GetDrawable(TEntry entry); + + private void entryBecameAlive(LifetimeEntry lifetimeEntry) + { + var entry = (TEntry)lifetimeEntry; + Debug.Assert(!aliveDrawableMap.ContainsKey(entry)); + + TDrawable drawable = GetDrawable(entry); + aliveDrawableMap[entry] = drawable; + AddDrawable(entry, drawable); + } + + /// + /// Add a corresponding to to this container. + /// + /// + /// Invoked when the entry became alive and a is obtained by . + /// + protected virtual void AddDrawable(TEntry entry, TDrawable drawable) => AddInternal(drawable); + + private void entryBecameDead(LifetimeEntry lifetimeEntry) + { + var entry = (TEntry)lifetimeEntry; + Debug.Assert(aliveDrawableMap.ContainsKey(entry)); + + TDrawable drawable = aliveDrawableMap[entry]; + aliveDrawableMap.Remove(entry); + RemoveDrawable(entry, drawable); + } + + /// + /// Remove a corresponding to from this container. + /// + /// + /// Invoked when the entry became dead. + /// + protected virtual void RemoveDrawable(TEntry entry, TDrawable drawable) => RemoveInternal(drawable); + + private void entryCrossedBoundary(LifetimeEntry lifetimeEntry, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction) + { + if (RemoveRewoundEntry && kind == LifetimeBoundaryKind.Start && direction == LifetimeBoundaryCrossingDirection.Backward) + Remove((TEntry)lifetimeEntry); + } + + /// + /// Remove all s. + /// + public void Clear() + { + foreach (var entry in Entries.ToArray()) + Remove(entry); + + Debug.Assert(aliveDrawableMap.Count == 0); + } + + protected override bool CheckChildrenLife() + { + bool aliveChanged = base.CheckChildrenLife(); + aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); + return aliveChanged; + } + } +} diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index e2e3c22618..0ab8b94e3f 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -68,10 +68,7 @@ namespace osu.Game.Rulesets.UI private bool frameStablePlayback = true; - /// - /// Whether to enable frame-stable playback. - /// - internal bool FrameStablePlayback + internal override bool FrameStablePlayback { get => frameStablePlayback; set @@ -431,6 +428,11 @@ namespace osu.Game.Rulesets.UI /// public abstract IFrameStableClock FrameStableClock { get; } + /// + /// Whether to enable frame-stable playback. + /// + internal abstract bool FrameStablePlayback { get; set; } + /// /// The mods which are to be applied. /// diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 83033b2dd5..fee77af0ba 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -3,35 +3,23 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.UI { - public class HitObjectContainer : CompositeDrawable, IHitObjectContainer + public class HitObjectContainer : PooledDrawableWithLifetimeContainer, IHitObjectContainer { - /// - /// All entries in this including dead entries. - /// - public IEnumerable Entries => allEntries; - - /// - /// All alive entries and s used by the entries. - /// - public IEnumerable<(HitObjectLifetimeEntry Entry, DrawableHitObject Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value)); - public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); - public IEnumerable AliveObjects => AliveInternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); + public IEnumerable AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime); /// /// Invoked when a is judged. @@ -59,34 +47,16 @@ namespace osu.Game.Rulesets.UI /// internal event Action HitObjectUsageFinished; - /// - /// The amount of time prior to the current time within which s should be considered alive. - /// - internal double PastLifetimeExtension { get; set; } - - /// - /// The amount of time after the current time within which s should be considered alive. - /// - internal double FutureLifetimeExtension { get; set; } - private readonly Dictionary startTimeMap = new Dictionary(); - private readonly Dictionary aliveDrawableMap = new Dictionary(); private readonly Dictionary nonPooledDrawableMap = new Dictionary(); - private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); - private readonly HashSet allEntries = new HashSet(); - [Resolved(CanBeNull = true)] private IPooledHitObjectProvider pooledObjectProvider { get; set; } public HitObjectContainer() { RelativeSizeAxes = Axes.Both; - - lifetimeManager.EntryBecameAlive += entryBecameAlive; - lifetimeManager.EntryBecameDead += entryBecameDead; - lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary; } protected override void LoadAsyncComplete() @@ -99,63 +69,41 @@ namespace osu.Game.Rulesets.UI #region Pooling support - public void Add(HitObjectLifetimeEntry entry) + public override bool Remove(HitObjectLifetimeEntry entry) { - allEntries.Add(entry); - lifetimeManager.AddEntry(entry); - } - - public bool Remove(HitObjectLifetimeEntry entry) - { - if (!lifetimeManager.RemoveEntry(entry)) return false; + if (!base.Remove(entry)) return false; // This logic is not in `Remove(DrawableHitObject)` because a non-pooled drawable may be removed by specifying its entry. if (nonPooledDrawableMap.Remove(entry, out var drawable)) removeDrawable(drawable); - allEntries.Remove(entry); return true; } - private void entryBecameAlive(LifetimeEntry lifetimeEntry) + protected sealed override DrawableHitObject GetDrawable(HitObjectLifetimeEntry entry) { - var entry = (HitObjectLifetimeEntry)lifetimeEntry; - Debug.Assert(!aliveDrawableMap.ContainsKey(entry)); + if (nonPooledDrawableMap.TryGetValue(entry, out var drawable)) + return drawable; - bool isPooled = !nonPooledDrawableMap.TryGetValue(entry, out var drawable); - drawable ??= pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null); - if (drawable == null) - throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}."); - - aliveDrawableMap[entry] = drawable; - - if (isPooled) - { - addDrawable(drawable); - HitObjectUsageBegan?.Invoke(entry.HitObject); - } - - OnAdd(drawable); + return pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null) ?? + throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}."); } - private void entryBecameDead(LifetimeEntry lifetimeEntry) + protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable) { - var entry = (HitObjectLifetimeEntry)lifetimeEntry; - Debug.Assert(aliveDrawableMap.ContainsKey(entry)); + if (nonPooledDrawableMap.ContainsKey(entry)) return; - var drawable = aliveDrawableMap[entry]; - bool isPooled = !nonPooledDrawableMap.ContainsKey(entry); + addDrawable(drawable); + HitObjectUsageBegan?.Invoke(entry.HitObject); + } + protected override void RemoveDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable) + { drawable.OnKilled(); - aliveDrawableMap.Remove(entry); + if (nonPooledDrawableMap.ContainsKey(entry)) return; - if (isPooled) - { - removeDrawable(drawable); - HitObjectUsageFinished?.Invoke(entry.HitObject); - } - - OnRemove(drawable); + removeDrawable(drawable); + HitObjectUsageFinished?.Invoke(entry.HitObject); } private void addDrawable(DrawableHitObject drawable) @@ -201,49 +149,8 @@ namespace osu.Game.Rulesets.UI public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject); - private void entryCrossedBoundary(LifetimeEntry entry, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction) - { - if (nonPooledDrawableMap.TryGetValue((HitObjectLifetimeEntry)entry, out var drawable)) - OnChildLifetimeBoundaryCrossed(new LifetimeBoundaryCrossedEvent(drawable, kind, direction)); - } - - protected virtual void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e) - { - } - #endregion - /// - /// Invoked after a is added to this container. - /// - protected virtual void OnAdd(DrawableHitObject drawableHitObject) - { - Debug.Assert(drawableHitObject.LoadState >= LoadState.Ready); - } - - /// - /// Invoked after a is removed from this container. - /// - protected virtual void OnRemove(DrawableHitObject drawableHitObject) - { - } - - public virtual void Clear() - { - lifetimeManager.ClearEntries(); - foreach (var drawable in nonPooledDrawableMap.Values) - removeDrawable(drawable); - nonPooledDrawableMap.Clear(); - Debug.Assert(InternalChildren.Count == 0 && startTimeMap.Count == 0 && aliveDrawableMap.Count == 0, "All hit objects should have been removed"); - } - - protected override bool CheckChildrenLife() - { - bool aliveChanged = base.CheckChildrenLife(); - aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); - return aliveChanged; - } - private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r); private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r); diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index cde4182f2d..f478e37e3e 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Layout; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -45,13 +47,6 @@ namespace osu.Game.Rulesets.UI.Scrolling timeRange.ValueChanged += _ => layoutCache.Invalidate(); } - public override void Clear() - { - base.Clear(); - - layoutComputed.Clear(); - } - /// /// Given a position in screen space, return the time within this column. /// @@ -147,17 +142,20 @@ namespace osu.Game.Rulesets.UI.Scrolling } } - protected override void OnAdd(DrawableHitObject drawableHitObject) + protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable) { - invalidateHitObject(drawableHitObject); - drawableHitObject.DefaultsApplied += invalidateHitObject; + base.AddDrawable(entry, drawable); + + invalidateHitObject(drawable); + drawable.DefaultsApplied += invalidateHitObject; } - protected override void OnRemove(DrawableHitObject drawableHitObject) + protected override void RemoveDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable) { - layoutComputed.Remove(drawableHitObject); + base.RemoveDrawable(entry, drawable); - drawableHitObject.DefaultsApplied -= invalidateHitObject; + drawable.DefaultsApplied -= invalidateHitObject; + layoutComputed.Remove(drawable); } private void invalidateHitObject(DrawableHitObject hitObject) @@ -206,6 +204,9 @@ namespace osu.Game.Rulesets.UI.Scrolling private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject) { + // Origin position may be relative to the parent size + Debug.Assert(hitObject.Parent != null); + float originAdjustment = 0.0f; // calculate the dimension of the part of the hitobject that should already be visible diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index bd4577fd57..b02e7ddb0d 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -31,6 +31,8 @@ namespace osu.Game.Screens.Backgrounds [Resolved] private IBindable beatmap { get; set; } + protected virtual bool AllowStoryboardBackground => true; + public BackgroundScreenDefault(bool animateOnEnter = true) : base(animateOnEnter) { @@ -110,6 +112,12 @@ namespace osu.Game.Screens.Backgrounds newBackground = new BeatmapBackground(beatmap.Value, backgroundName); break; + case BackgroundSource.BeatmapWithStoryboard: + newBackground = AllowStoryboardBackground + ? new BeatmapBackgroundWithStoryboard(beatmap.Value, backgroundName) + : new BeatmapBackground(beatmap.Value, backgroundName); + break; + default: newBackground = new SkinnedBackground(skin.Value, backgroundName); break; diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index fcbc6fae15..3fbb55872b 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -100,7 +100,13 @@ namespace osu.Game.Screens.Play { // The source is stopped by a frequency fade first. if (isPaused.NewValue) - this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => AdjustableSource.Stop()); + { + this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => + { + if (IsPaused.Value == isPaused.NewValue) + AdjustableSource.Stop(); + }); + } else this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 20012d0282..94e67107c9 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -81,10 +81,6 @@ namespace osu.Game.Screens.Play [Resolved] private ScoreManager scoreManager { get; set; } - private RulesetInfo rulesetInfo; - - private Ruleset ruleset; - [Resolved] private IAPIProvider api { get; set; } @@ -94,6 +90,10 @@ namespace osu.Game.Screens.Play [Resolved] private SpectatorClient spectatorClient { get; set; } + protected Ruleset GameplayRuleset { get; private set; } + + protected GameplayBeatmap GameplayBeatmap { get; private set; } + private Sample sampleRestart; public BreakOverlay BreakOverlay; @@ -144,8 +144,6 @@ namespace osu.Game.Screens.Play Configuration = configuration ?? new PlayerConfiguration(); } - protected GameplayBeatmap GameplayBeatmap { get; private set; } - private ScreenSuspensionHandler screenSuspension; private DependencyContainer dependencies; @@ -164,7 +162,7 @@ namespace osu.Game.Screens.Play // ensure the score is in a consistent state with the current player. Score.ScoreInfo.Beatmap = Beatmap.Value.BeatmapInfo; - Score.ScoreInfo.Ruleset = rulesetInfo; + Score.ScoreInfo.Ruleset = GameplayRuleset.RulesetInfo; Score.ScoreInfo.Mods = Mods.Value.ToArray(); PrepareReplay(); @@ -211,16 +209,16 @@ namespace osu.Game.Screens.Play if (game is OsuGame osuGame) LocalUserPlaying.BindTo(osuGame.LocalUserPlaying); - DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); + DrawableRuleset = GameplayRuleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); dependencies.CacheAs(DrawableRuleset); - ScoreProcessor = ruleset.CreateScoreProcessor(); + ScoreProcessor = GameplayRuleset.CreateScoreProcessor(); ScoreProcessor.ApplyBeatmap(playableBeatmap); ScoreProcessor.Mods.BindTo(Mods); dependencies.CacheAs(ScoreProcessor); - HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); + HealthProcessor = GameplayRuleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); HealthProcessor.ApplyBeatmap(playableBeatmap); dependencies.CacheAs(HealthProcessor); @@ -239,7 +237,7 @@ namespace osu.Game.Screens.Play // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation // full access to all skin sources. - var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap)); + var rulesetSkinProvider = new SkinProvidingContainer(GameplayRuleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap)); // 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. @@ -254,7 +252,7 @@ namespace osu.Game.Screens.Play // also give the HUD a ruleset container to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there. - var hudRulesetContainer = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap)); + var hudRulesetContainer = new SkinProvidingContainer(GameplayRuleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap)); // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. GameplayClockContainer.Add(hudRulesetContainer.WithChild(createOverlayComponents(Beatmap.Value))); @@ -480,18 +478,18 @@ namespace osu.Game.Screens.Play if (Beatmap.Value.Beatmap == null) throw new InvalidOperationException("Beatmap was not loaded"); - rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset; - ruleset = rulesetInfo.CreateInstance(); + var rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset; + GameplayRuleset = rulesetInfo.CreateInstance(); try { - playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Mods.Value); + playable = Beatmap.Value.GetPlayableBeatmap(GameplayRuleset.RulesetInfo, Mods.Value); } catch (BeatmapInvalidForRulesetException) { // A playable beatmap may not be creatable with the user's preferred ruleset, so try using the beatmap's default ruleset rulesetInfo = Beatmap.Value.BeatmapInfo.Ruleset; - ruleset = rulesetInfo.CreateInstance(); + GameplayRuleset = rulesetInfo.CreateInstance(); playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, Mods.Value); } @@ -585,6 +583,29 @@ namespace osu.Game.Screens.Play /// The destination time to seek to. public void Seek(double time) => GameplayClockContainer.Seek(time); + private ScheduledDelegate frameStablePlaybackResetDelegate; + + /// + /// Seeks to a specific time in gameplay, bypassing frame stability. + /// + /// + /// Intermediate hitobject judgements may not be applied or reverted correctly during this seek. + /// + /// The destination time to seek to. + internal void NonFrameStableSeek(double time) + { + if (frameStablePlaybackResetDelegate?.Cancelled == false && !frameStablePlaybackResetDelegate.Completed) + frameStablePlaybackResetDelegate.RunTask(); + + bool wasFrameStable = DrawableRuleset.FrameStablePlayback; + DrawableRuleset.FrameStablePlayback = false; + + Seek(time); + + // Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek. + frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable); + } + /// /// Restart gameplay via a parent . /// This can be called from a child screen in order to trigger the restart process. @@ -918,11 +939,10 @@ namespace osu.Game.Screens.Play /// Creates the player's . /// /// The . - protected virtual Score CreateScore() => - new Score - { - ScoreInfo = new ScoreInfo { User = api.LocalUser.Value }, - }; + protected virtual Score CreateScore() => new Score + { + ScoreInfo = new ScoreInfo { User = api.LocalUser.Value }, + }; /// /// Imports the player's to the local database. diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index 67471dff90..0286b6b8a6 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -1,14 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Spectator; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Types; using osu.Game.Scoring; using osu.Game.Screens.Ranking; @@ -16,11 +16,11 @@ namespace osu.Game.Screens.Play { public class SpectatorPlayer : Player { - private readonly Score score; - [Resolved] private SpectatorClient spectatorClient { get; set; } + private readonly Score score; + protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap public SpectatorPlayer(Score score) @@ -28,11 +28,6 @@ namespace osu.Game.Screens.Play this.score = score; } - protected override Score CreateScore() => score; - - protected override ResultsScreen CreateResults(ScoreInfo score) - => new SpectatorResultsScreen(score); - [BackgroundDependencyLoader] private void load() { @@ -48,25 +43,66 @@ namespace osu.Game.Screens.Play }); } + protected override void StartGameplay() + { + base.StartGameplay(); + + spectatorClient.OnNewFrames += userSentFrames; + seekToGameplay(); + } + + private void userSentFrames(int userId, FrameDataBundle bundle) + { + if (userId != score.ScoreInfo.User.Id) + return; + + if (!LoadedBeatmapSuccessfully) + return; + + if (!this.IsCurrentScreen()) + return; + + foreach (var frame in bundle.Frames) + { + IConvertibleReplayFrame convertibleFrame = GameplayRuleset.CreateConvertibleReplayFrame(); + convertibleFrame.FromLegacy(frame, GameplayBeatmap.PlayableBeatmap); + + var convertedFrame = (ReplayFrame)convertibleFrame; + convertedFrame.Time = frame.Time; + + score.Replay.Frames.Add(convertedFrame); + } + + seekToGameplay(); + } + + private bool seekedToGameplay; + + private void seekToGameplay() + { + if (seekedToGameplay || score.Replay.Frames.Count == 0) + return; + + NonFrameStableSeek(score.Replay.Frames[0].Time); + + seekedToGameplay = true; + } + + protected override Score CreateScore() => score; + + protected override ResultsScreen CreateResults(ScoreInfo score) + => new SpectatorResultsScreen(score); + protected override void PrepareReplay() { DrawableRuleset?.SetReplayScore(score); } - protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) - { - // if we already have frames, start gameplay at the point in time they exist, should they be too far into the beatmap. - double? firstFrameTime = score.Replay.Frames.FirstOrDefault()?.Time; - - if (firstFrameTime == null || firstFrameTime <= gameplayStart + 5000) - return base.CreateGameplayClockContainer(beatmap, gameplayStart); - - return new MasterGameplayClockContainer(beatmap, firstFrameTime.Value, true); - } - public override bool OnExiting(IScreen next) { spectatorClient.OnUserBeganPlaying -= userBeganPlaying; + spectatorClient.OnNewFrames -= userSentFrames; + return base.OnExiting(next); } @@ -85,7 +121,10 @@ namespace osu.Game.Screens.Play base.Dispose(isDisposing); if (spectatorClient != null) + { spectatorClient.OnUserBeganPlaying -= userBeganPlaying; + spectatorClient.OnNewFrames -= userSentFrames; + } } } } diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 9a20bb58b8..8fc9222f59 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -15,8 +15,6 @@ using osu.Game.Database; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Rulesets; -using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Replays.Types; using osu.Game.Scoring; using osu.Game.Users; @@ -71,8 +69,6 @@ namespace osu.Game.Screens.Spectate playingUserStates.BindTo(spectatorClient.PlayingUserStates); playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); - spectatorClient.OnNewFrames += userSentFrames; - managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); @@ -197,29 +193,6 @@ namespace osu.Game.Screens.Spectate Schedule(() => StartGameplay(userId, gameplayState)); } - private void userSentFrames(int userId, FrameDataBundle bundle) - { - if (!userMap.ContainsKey(userId)) - return; - - if (!gameplayStates.TryGetValue(userId, out var gameplayState)) - return; - - // The ruleset instance should be guaranteed to be in sync with the score via ScoreLock. - Debug.Assert(gameplayState.Ruleset != null && gameplayState.Ruleset.RulesetInfo.Equals(gameplayState.Score.ScoreInfo.Ruleset)); - - foreach (var frame in bundle.Frames) - { - IConvertibleReplayFrame convertibleFrame = gameplayState.Ruleset.CreateConvertibleReplayFrame(); - convertibleFrame.FromLegacy(frame, gameplayState.Beatmap.Beatmap); - - var convertedFrame = (ReplayFrame)convertibleFrame; - convertedFrame.Time = frame.Time; - - gameplayState.Score.Replay.Frames.Add(convertedFrame); - } - } - /// /// Invoked when a spectated user's state has changed. /// @@ -260,8 +233,6 @@ namespace osu.Game.Screens.Spectate if (spectatorClient != null) { - spectatorClient.OnNewFrames -= userSentFrames; - foreach (var (userId, _) in userMap) spectatorClient.StopWatchingUser(userId); } diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index be9a015ab2..a4c78f24e3 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual { dummyAPI = new DummyAPIAccess(); Dependencies.CacheAs(dummyAPI); - Add(dummyAPI); + base.Content.Add(dummyAPI); } return Dependencies; diff --git a/osu.Game/Utils/StatelessRNG.cs b/osu.Game/Utils/StatelessRNG.cs index 118b08fe30..cd169229e3 100644 --- a/osu.Game/Utils/StatelessRNG.cs +++ b/osu.Game/Utils/StatelessRNG.cs @@ -75,5 +75,10 @@ namespace osu.Game.Utils /// public static float NextSingle(int seed, int series = 0) => (float)(NextULong(seed, series) & ((1 << 24) - 1)) / (1 << 24); // float has 24-bit precision + + /// + /// Compute a random floating point value between and from given seed and series number. + /// + public static float NextSingle(float min, float max, int seed, int series = 0) => min + NextSingle(seed, series) * (max - min); } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 49b86ad56e..21a890014a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -34,7 +34,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index cbb6a21fd1..bf080e4def 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - +