diff --git a/osu.Android.props b/osu.Android.props
index e95c7e6619..395470824f 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index cffcea22c2..063e02d349 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -20,7 +20,8 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
- [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })]
+ [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-archive")]
+ [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed", "application/x-osu-archive" })]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity
{
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
index 1248409b2a..09362929d2 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
@@ -4,10 +4,8 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
-using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
@@ -21,12 +19,6 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneCatchModHidden : ModTestScene
{
- [BackgroundDependencyLoader]
- private void load()
- {
- LocalConfig.SetValue(OsuSetting.IncreaseFirstObjectVisibility, false);
- }
-
[Test]
public void TestJuiceStream()
{
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/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
index 7bad4c79cb..f9e106f097 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
@@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Catch.Mods
}
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
- {
- }
+ => ApplyNormalVisibilityState(hitObject, state);
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
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/Scoring/CatchHitWindows.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
deleted file mode 100644
index 0a444d923e..0000000000
--- a/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
+++ /dev/null
@@ -1,22 +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.Game.Rulesets.Scoring;
-
-namespace osu.Game.Rulesets.Catch.Scoring
-{
- public class CatchHitWindows : HitWindows
- {
- public override bool IsHitResultAllowed(HitResult result)
- {
- switch (result)
- {
- case HitResult.Great:
- case HitResult.Miss:
- return true;
- }
-
- return false;
- }
- }
-}
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 b8c4d8f036..2e4f2839f1 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/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/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index b7458b5695..4a2a18ffd6 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
@@ -152,7 +152,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
while (Math.Abs(aimRotation - Arrow.Rotation) > 180)
aimRotation += aimRotation < Arrow.Rotation ? 360 : -360;
- if (!hasRotation)
+ // The clock may be paused in a scenario like the editor.
+ if (!hasRotation || !Clock.IsRunning)
{
Arrow.Rotation = aimRotation;
hasRotation = true;
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/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
new file mode 100644
index 0000000000..09fe9b3767
--- /dev/null
+++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
@@ -0,0 +1,90 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Screens;
+using osu.Framework.Testing;
+using osu.Game.Configuration;
+using osu.Game.Graphics.Backgrounds;
+using osu.Game.Online.API;
+using osu.Game.Screens;
+using osu.Game.Screens.Backgrounds;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Background
+{
+ [TestFixture]
+ public class TestSceneBackgroundScreenDefault : OsuTestScene
+ {
+ private BackgroundScreenStack stack;
+ private BackgroundScreenDefault screen;
+
+ private Graphics.Backgrounds.Background getCurrentBackground() => screen.ChildrenOfType().FirstOrDefault();
+
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create background stack", () => Child = stack = new BackgroundScreenStack());
+ AddStep("push default screen", () => stack.Push(screen = new BackgroundScreenDefault(false)));
+ AddUntilStep("wait for screen to load", () => screen.IsCurrentScreen());
+ }
+
+ [Test]
+ public void TestTogglingStoryboardSwitchesBackgroundType()
+ {
+ setSupporter(true);
+
+ setSourceMode(BackgroundSource.Beatmap);
+ AddUntilStep("is beatmap background", () => getCurrentBackground() is BeatmapBackground);
+
+ setSourceMode(BackgroundSource.BeatmapWithStoryboard);
+ AddUntilStep("is storyboard background", () => getCurrentBackground() is BeatmapBackgroundWithStoryboard);
+ }
+
+ [Test]
+ public void TestTogglingSupporterTogglesBeatmapBackground()
+ {
+ setSourceMode(BackgroundSource.Beatmap);
+
+ setSupporter(true);
+ AddUntilStep("is beatmap background", () => getCurrentBackground() is BeatmapBackground);
+
+ setSupporter(false);
+ AddUntilStep("is default background", () => !(getCurrentBackground() is BeatmapBackground));
+
+ setSupporter(true);
+ AddUntilStep("is beatmap background", () => getCurrentBackground() is BeatmapBackground);
+ }
+
+ [Test]
+ public void TestBeatmapDoesntReloadOnNoChange()
+ {
+ BeatmapBackground last = null;
+
+ setSourceMode(BackgroundSource.Beatmap);
+ setSupporter(true);
+
+ AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground() as BeatmapBackground) != null);
+ AddAssert("next doesn't load new background", () => screen.Next() == false);
+
+ // doesn't really need to be checked but might as well.
+ AddWaitStep("wait a bit", 5);
+ AddUntilStep("ensure same background instance", () => last == getCurrentBackground());
+ }
+
+ private void setSourceMode(BackgroundSource source) =>
+ AddStep("set background mode to beatmap", () => config.SetValue(OsuSetting.MenuBackgroundSource, source));
+
+ private void setSupporter(bool isSupporter) =>
+ AddStep($"set supporter {isSupporter}", () => ((DummyAPIAccess)API).LocalUser.Value = new User
+ {
+ IsSupporter = isSupporter,
+ Id = API.LocalUser.Value.Id + 1,
+ });
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
index 2c5443fe08..2a12577ad8 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
@@ -4,14 +4,15 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics.Sprites;
-using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
@@ -29,15 +30,22 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneHitErrorMeter : OsuTestScene
{
- [Cached]
- private ScoreProcessor scoreProcessor = new ScoreProcessor();
+ [Cached(typeof(ScoreProcessor))]
+ private TestScoreProcessor scoreProcessor = new TestScoreProcessor();
[Cached(typeof(DrawableRuleset))]
private TestDrawableRuleset drawableRuleset = new TestDrawableRuleset();
- public TestSceneHitErrorMeter()
+ [SetUpSteps]
+ public void SetUp()
{
- recreateDisplay(new OsuHitWindows(), 5);
+ AddStep("reset score processor", () => scoreProcessor.Reset());
+ }
+
+ [Test]
+ public void TestBasic()
+ {
+ AddStep("create display", () => recreateDisplay(new OsuHitWindows(), 5));
AddRepeatStep("New random judgement", () => newJudgement(), 40);
@@ -45,12 +53,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddRepeatStep("New max positive", () => newJudgement(drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20);
AddStep("New fixed judgement (50ms)", () => newJudgement(50));
+ ScheduledDelegate del = null;
AddStep("Judgement barrage", () =>
{
int runCount = 0;
- ScheduledDelegate del = null;
-
del = Scheduler.AddDelayed(() =>
{
newJudgement(runCount++ / 10f);
@@ -60,6 +67,7 @@ namespace osu.Game.Tests.Visual.Gameplay
del?.Cancel();
}, 10, true);
});
+ AddUntilStep("wait for barrage", () => del.Cancelled);
}
[Test]
@@ -84,10 +92,21 @@ namespace osu.Game.Tests.Visual.Gameplay
}
[Test]
- public void TestCatch()
+ public void TestEmpty()
{
- AddStep("OD 1", () => recreateDisplay(new CatchHitWindows(), 1));
- AddStep("OD 10", () => recreateDisplay(new CatchHitWindows(), 10));
+ AddStep("empty windows", () => recreateDisplay(HitWindows.Empty, 5));
+
+ AddStep("hit", () => newJudgement());
+ AddAssert("no bars added", () => !this.ChildrenOfType().Any());
+ AddAssert("circle added", () =>
+ this.ChildrenOfType().All(
+ meter => meter.ChildrenOfType().Count() == 1));
+
+ AddStep("miss", () => newJudgement(50, HitResult.Miss));
+ AddAssert("no bars added", () => !this.ChildrenOfType().Any());
+ AddAssert("circle added", () =>
+ this.ChildrenOfType().All(
+ meter => meter.ChildrenOfType().Count() == 2));
}
private void recreateDisplay(HitWindows hitWindows, float overallDifficulty)
@@ -154,12 +173,12 @@ namespace osu.Game.Tests.Visual.Gameplay
});
}
- private void newJudgement(double offset = 0)
+ private void newJudgement(double offset = 0, HitResult result = HitResult.Perfect)
{
scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = drawableRuleset.HitWindows }, new Judgement())
{
TimeOffset = offset == 0 ? RNG.Next(-150, 150) : offset,
- Type = HitResult.Perfect,
+ Type = result,
});
}
@@ -177,6 +196,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; }
@@ -198,5 +218,10 @@ namespace osu.Game.Tests.Visual.Gameplay
public override void CancelResume() => throw new NotImplementedException();
}
+
+ private class TestScoreProcessor : ScoreProcessor
+ {
+ public void Reset() => base.Reset(false);
+ }
}
}
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/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs
index 086cc573d5..b53cc659f7 100644
--- a/osu.Game/Collections/CollectionManager.cs
+++ b/osu.Game/Collections/CollectionManager.cs
@@ -264,14 +264,18 @@ namespace osu.Game.Collections
using (var sw = new SerializationWriter(storage.GetStream(database_name, FileAccess.Write)))
{
sw.Write(database_version);
- sw.Write(Collections.Count);
- foreach (var c in Collections)
+ var collectionsCopy = Collections.ToArray();
+ sw.Write(collectionsCopy.Length);
+
+ foreach (var c in collectionsCopy)
{
sw.Write(c.Name.Value);
- sw.Write(c.Beatmaps.Count);
- foreach (var b in c.Beatmaps)
+ var beatmapsCopy = c.Beatmaps.ToArray();
+ sw.Write(beatmapsCopy.Length);
+
+ foreach (var b in beatmapsCopy)
sw.Write(b.MD5Hash);
}
}
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/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..6bcfaac907 100644
--- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
+++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
@@ -5,8 +5,8 @@ using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Utils;
using osu.Framework.Threading;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Backgrounds;
@@ -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)
{
@@ -51,14 +53,41 @@ namespace osu.Game.Screens.Backgrounds
mode.ValueChanged += _ => Next();
beatmap.ValueChanged += _ => Next();
introSequence.ValueChanged += _ => Next();
- seasonalBackgroundLoader.SeasonalBackgroundChanged += Next;
+ seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Next();
currentDisplay = RNG.Next(0, background_count);
Next();
}
- private void display(Background newBackground)
+ private ScheduledDelegate nextTask;
+ private CancellationTokenSource cancellationTokenSource;
+
+ ///
+ /// Request loading the next background.
+ ///
+ /// Whether a new background was queued for load. May return false if the current background is still valid.
+ public bool Next()
+ {
+ var nextBackground = createBackground();
+
+ // in the case that the background hasn't changed, we want to avoid cancelling any tasks that could still be loading.
+ if (nextBackground == background)
+ return false;
+
+ cancellationTokenSource?.Cancel();
+ cancellationTokenSource = new CancellationTokenSource();
+
+ nextTask?.Cancel();
+ nextTask = Scheduler.AddDelayed(() =>
+ {
+ LoadComponentAsync(nextBackground, displayNext, cancellationTokenSource.Token);
+ }, 100);
+
+ return true;
+ }
+
+ private void displayNext(Background newBackground)
{
background?.FadeOut(800, Easing.InOutSine);
background?.Expire();
@@ -67,62 +96,51 @@ namespace osu.Game.Screens.Backgrounds
currentDisplay++;
}
- private ScheduledDelegate nextTask;
- private CancellationTokenSource cancellationTokenSource;
-
- public void Next()
- {
- nextTask?.Cancel();
- cancellationTokenSource?.Cancel();
- cancellationTokenSource = new CancellationTokenSource();
- nextTask = Scheduler.AddDelayed(() => LoadComponentAsync(createBackground(), display, cancellationTokenSource.Token), 100);
- }
-
private Background createBackground()
{
- Background newBackground;
- string backgroundName;
+ // seasonal background loading gets highest priority.
+ Background newBackground = seasonalBackgroundLoader.LoadNextBackground();
- var seasonalBackground = seasonalBackgroundLoader.LoadNextBackground();
-
- if (seasonalBackground != null)
- {
- seasonalBackground.Depth = currentDisplay;
- return seasonalBackground;
- }
-
- switch (introSequence.Value)
- {
- case IntroSequence.Welcome:
- backgroundName = "Intro/Welcome/menu-background";
- break;
-
- default:
- backgroundName = $@"Menu/menu-background-{currentDisplay % background_count + 1}";
- break;
- }
-
- if (user.Value?.IsSupporter ?? false)
+ if (newBackground == null && user.Value?.IsSupporter == true)
{
switch (mode.Value)
{
case BackgroundSource.Beatmap:
- newBackground = new BeatmapBackground(beatmap.Value, backgroundName);
- break;
+ case BackgroundSource.BeatmapWithStoryboard:
+ {
+ if (mode.Value == BackgroundSource.BeatmapWithStoryboard && AllowStoryboardBackground)
+ newBackground = new BeatmapBackgroundWithStoryboard(beatmap.Value, getBackgroundTextureName());
+ newBackground ??= new BeatmapBackground(beatmap.Value, getBackgroundTextureName());
+
+ // this method is called in many cases where the beatmap hasn't changed (ie. on screen transitions).
+ // if a background is already displayed for the requested beatmap, we don't want to load it again.
+ if (background?.GetType() == newBackground.GetType() &&
+ (background as BeatmapBackground)?.Beatmap == beatmap.Value)
+ return background;
- default:
- newBackground = new SkinnedBackground(skin.Value, backgroundName);
break;
+ }
}
}
- else
- newBackground = new Background(backgroundName);
+ newBackground ??= new Background(getBackgroundTextureName());
newBackground.Depth = currentDisplay;
return newBackground;
}
+ private string getBackgroundTextureName()
+ {
+ switch (introSequence.Value)
+ {
+ case IntroSequence.Welcome:
+ return @"Intro/Welcome/menu-background";
+
+ default:
+ return $@"Menu/menu-background-{currentDisplay % background_count + 1}";
+ }
+ }
+
private class SkinnedBackground : Background
{
private readonly Skin skin;
diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
index 5d0263772d..0412085d1d 100644
--- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
@@ -214,7 +214,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
protected override void OnNewJudgement(JudgementResult judgement)
{
- if (!judgement.IsHit)
+ if (!judgement.IsHit || judgement.HitObject.HitWindows?.WindowFor(HitResult.Miss) == 0)
return;
if (judgementsContainer.Count > max_concurrent_judgements)
@@ -244,7 +244,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
private float getRelativeJudgementPosition(double value) => Math.Clamp((float)((value / maxHitWindow) + 1) / 2, 0, 1);
- private class JudgementLine : CompositeDrawable
+ internal class JudgementLine : CompositeDrawable
{
private const int judgement_fade_duration = 5000;
diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs
index e9ccbcdae2..86c0de8855 100644
--- a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
}
}
- private class HitErrorCircle : Container
+ internal class HitErrorCircle : Container
{
public bool IsRemoved { get; private set; }
diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs
index b0f9928b13..17a6e772fb 100644
--- a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs
@@ -32,15 +32,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{
base.LoadComplete();
- processor.NewJudgement += onNewJudgement;
- }
-
- private void onNewJudgement(JudgementResult result)
- {
- if (result.HitObject.HitWindows?.WindowFor(HitResult.Miss) == 0)
- return;
-
- OnNewJudgement(result);
+ processor.NewJudgement += OnNewJudgement;
}
protected abstract void OnNewJudgement(JudgementResult judgement);
@@ -74,7 +66,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
base.Dispose(isDisposing);
if (processor != null)
- processor.NewJudgement -= onNewJudgement;
+ processor.NewJudgement -= OnNewJudgement;
}
}
}
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/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs
index 33e8c137f4..3fcca74fb8 100644
--- a/osu.Game/Skinning/PoolableSkinnableSample.cs
+++ b/osu.Game/Skinning/PoolableSkinnableSample.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -70,22 +71,48 @@ namespace osu.Game.Skinning
updateSample();
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ CurrentSkin.SourceChanged += skinChangedImmediate;
+ }
+
+ private void skinChangedImmediate()
+ {
+ // Clean up the previous sample immediately on a source change.
+ // This avoids a potential call to Play() of an already disposed sample (samples are disposed along with the skin, but SkinChanged is scheduled).
+ clearPreviousSamples();
+ }
+
protected override void SkinChanged(ISkinSource skin)
{
base.SkinChanged(skin);
updateSample();
}
+ ///
+ /// Whether this sample was playing before a skin source change.
+ ///
+ private bool wasPlaying;
+
+ private void clearPreviousSamples()
+ {
+ // only run if the samples aren't already cleared.
+ // this ensures the "wasPlaying" state is stored correctly even if multiple clear calls are executed.
+ if (!sampleContainer.Any()) return;
+
+ wasPlaying = Playing;
+
+ sampleContainer.Clear();
+ Sample = null;
+ }
+
private void updateSample()
{
if (sampleInfo == null)
return;
- bool wasPlaying = Playing;
-
- sampleContainer.Clear();
- Sample = null;
-
var sample = CurrentSkin.GetSample(sampleInfo);
if (sample == null)
@@ -146,6 +173,14 @@ namespace osu.Game.Skinning
}
}
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (CurrentSkin != null)
+ CurrentSkin.SourceChanged -= skinChangedImmediate;
+ }
+
#region Re-expose AudioContainer
public BindableNumber Volume => sampleContainer.Volume;
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..9ecab1ee48 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..e66f125985 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -93,7 +93,7 @@
-
+