Merge branch 'master' into fix-skin-sample-lookup

This commit is contained in:
Dean Herbert
2021-06-08 17:37:36 +09:00
committed by GitHub
55 changed files with 770 additions and 514 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.525.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.525.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.601.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.608.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -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)] [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 = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", 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" })] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity public class OsuGameActivity : AndroidGameActivity
{ {

View File

@ -4,10 +4,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
@ -21,12 +19,6 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
public class TestSceneCatchModHidden : ModTestScene public class TestSceneCatchModHidden : ModTestScene
{ {
[BackgroundDependencyLoader]
private void load()
{
LocalConfig.SetValue(OsuSetting.IncreaseFirstObjectVisibility, false);
}
[Test] [Test]
public void TestJuiceStream() public void TestJuiceStream()
{ {

View File

@ -216,7 +216,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true)); AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
AddStep("catch fruit", () => attemptCatch(new Fruit())); AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("correct hit lighting colour", () => AddAssert("correct hit lighting colour", () =>
catcher.ChildrenOfType<HitExplosion>().First()?.ObjectColour == fruitColour); catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == fruitColour);
} }
[Test] [Test]

View File

@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Catch.Mods
} }
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{ => ApplyNormalVisibilityState(hitObject, state);
}
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
{ {

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Rulesets.Catch.Skinning.Default;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
@ -9,21 +8,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
/// <summary> /// <summary>
/// Represents a <see cref="Fruit"/> caught by the catcher. /// Represents a <see cref="Fruit"/> caught by the catcher.
/// </summary> /// </summary>
public class CaughtFruit : CaughtObject, IHasFruitState public class CaughtFruit : CaughtObject
{ {
public Bindable<FruitVisualRepresentation> VisualRepresentation { get; } = new Bindable<FruitVisualRepresentation>();
public CaughtFruit() public CaughtFruit()
: base(CatchSkinComponents.Fruit, _ => new FruitPiece()) : base(CatchSkinComponents.Fruit, _ => new FruitPiece())
{ {
} }
public override void CopyStateFrom(IHasCatchObjectState objectState)
{
base.CopyStateFrom(objectState);
var fruitState = (IHasFruitState)objectState;
VisualRepresentation.Value = fruitState.VisualRepresentation.Value;
}
} }
} }

View File

@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public PalpableCatchHitObject HitObject { get; private set; } public PalpableCatchHitObject HitObject { get; private set; }
public Bindable<Color4> AccentColour { get; } = new Bindable<Color4>(); public Bindable<Color4> AccentColour { get; } = new Bindable<Color4>();
public Bindable<bool> HyperDash { get; } = new Bindable<bool>(); public Bindable<bool> HyperDash { get; } = new Bindable<bool>();
public Bindable<int> IndexInBeatmap { get; } = new Bindable<int>();
public Vector2 DisplaySize => Size * Scale; public Vector2 DisplaySize => Size * Scale;
@ -51,6 +52,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Rotation = objectState.DisplayRotation; Rotation = objectState.DisplayRotation;
AccentColour.Value = objectState.AccentColour.Value; AccentColour.Value = objectState.AccentColour.Value;
HyperDash.Value = objectState.HyperDash.Value; HyperDash.Value = objectState.HyperDash.Value;
IndexInBeatmap.Value = objectState.IndexInBeatmap.Value;
} }
protected override void FreeAfterUse() protected override void FreeAfterUse()

View File

@ -3,17 +3,14 @@
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableFruit : DrawablePalpableCatchHitObject, IHasFruitState public class DrawableFruit : DrawablePalpableCatchHitObject
{ {
public Bindable<FruitVisualRepresentation> VisualRepresentation { get; } = new Bindable<FruitVisualRepresentation>();
public DrawableFruit() public DrawableFruit()
: this(null) : this(null)
{ {
@ -27,11 +24,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
IndexInBeatmap.BindValueChanged(change =>
{
VisualRepresentation.Value = (FruitVisualRepresentation)(change.NewValue % 4);
}, true);
ScalingContainer.Child = new SkinnableDrawable( ScalingContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Fruit), new CatchSkinComponent(CatchSkinComponents.Fruit),
_ => new FruitPiece()); _ => new FruitPiece());
@ -44,12 +36,4 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40); ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40);
} }
} }
public enum FruitVisualRepresentation
{
Pear,
Grape,
Pineapple,
Raspberry,
}
} }

View File

@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Bindable<bool> HyperDash { get; } Bindable<bool> HyperDash { get; }
Bindable<int> IndexInBeatmap { get; }
Vector2 DisplaySize { get; } Vector2 DisplaySize { get; }
float DisplayRotation { get; } float DisplayRotation { get; }

View File

@ -1,15 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
/// <summary>
/// Provides a visual state of a <see cref="Fruit"/>.
/// </summary>
public interface IHasFruitState : IHasCatchObjectState
{
Bindable<FruitVisualRepresentation> VisualRepresentation { get; }
}
}

View File

@ -9,5 +9,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public class Fruit : PalpableCatchHitObject public class Fruit : PalpableCatchHitObject
{ {
public override Judgement CreateJudgement() => new CatchJudgement(); public override Judgement CreateJudgement() => new CatchJudgement();
public static FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4);
} }
} }

View File

@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Catch.Objects
{
public enum FruitVisualRepresentation
{
Pear,
Grape,
Pineapple,
Raspberry,
}
}

View File

@ -1,22 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.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;
}
}
}

View File

@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
{ {
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>(); public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
public readonly Bindable<bool> HyperDash = new Bindable<bool>(); public readonly Bindable<bool> HyperDash = new Bindable<bool>();
public readonly Bindable<int> IndexInBeatmap = new Bindable<int>();
[Resolved] [Resolved]
protected IHasCatchObjectState ObjectState { get; private set; } protected IHasCatchObjectState ObjectState { get; private set; }
@ -37,6 +38,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
AccentColour.BindTo(ObjectState.AccentColour); AccentColour.BindTo(ObjectState.AccentColour);
HyperDash.BindTo(ObjectState.HyperDash); HyperDash.BindTo(ObjectState.HyperDash);
IndexInBeatmap.BindTo(ObjectState.IndexInBeatmap);
HyperDash.BindValueChanged(hyper => HyperDash.BindValueChanged(hyper =>
{ {

View File

@ -3,7 +3,7 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects;
namespace osu.Game.Rulesets.Catch.Skinning.Default namespace osu.Game.Rulesets.Catch.Skinning.Default
{ {
@ -39,8 +39,10 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
{ {
base.LoadComplete(); base.LoadComplete();
var fruitState = (IHasFruitState)ObjectState; IndexInBeatmap.BindValueChanged(index =>
VisualRepresentation.BindTo(fruitState.VisualRepresentation); {
VisualRepresentation.Value = Fruit.GetVisualRepresentation(index.NewValue);
}, true);
} }
} }
} }

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Default namespace osu.Game.Rulesets.Catch.Skinning.Default

View File

@ -3,7 +3,7 @@
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
namespace osu.Game.Rulesets.Catch.Skinning namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{ {
public class LegacyBananaPiece : LegacyCatchHitObjectPiece public class LegacyBananaPiece : LegacyCatchHitObjectPiece
{ {

View File

@ -13,12 +13,13 @@ using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{ {
public abstract class LegacyCatchHitObjectPiece : PoolableDrawable public abstract class LegacyCatchHitObjectPiece : PoolableDrawable
{ {
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>(); public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
public readonly Bindable<bool> HyperDash = new Bindable<bool>(); public readonly Bindable<bool> HyperDash = new Bindable<bool>();
public readonly Bindable<int> IndexInBeatmap = new Bindable<int>();
private readonly Sprite colouredSprite; private readonly Sprite colouredSprite;
private readonly Sprite overlaySprite; private readonly Sprite overlaySprite;
@ -64,6 +65,7 @@ namespace osu.Game.Rulesets.Catch.Skinning
AccentColour.BindTo(ObjectState.AccentColour); AccentColour.BindTo(ObjectState.AccentColour);
HyperDash.BindTo(ObjectState.HyperDash); HyperDash.BindTo(ObjectState.HyperDash);
IndexInBeatmap.BindTo(ObjectState.IndexInBeatmap);
hyperSprite.Colour = Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value ?? hyperSprite.Colour = Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value ??
Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ?? Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??

View File

@ -4,7 +4,7 @@
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{ {
public class LegacyDropletPiece : LegacyCatchHitObjectPiece public class LegacyDropletPiece : LegacyCatchHitObjectPiece
{ {

View File

@ -1,23 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{ {
internal class LegacyFruitPiece : LegacyCatchHitObjectPiece internal class LegacyFruitPiece : LegacyCatchHitObjectPiece
{ {
public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
var fruitState = (IHasFruitState)ObjectState; IndexInBeatmap.BindValueChanged(index =>
VisualRepresentation.BindTo(fruitState.VisualRepresentation); {
setTexture(Fruit.GetVisualRepresentation(index.NewValue));
VisualRepresentation.BindValueChanged(visual => setTexture(visual.NewValue), true); }, true);
} }
private void setTexture(FruitVisualRepresentation visualRepresentation) private void setTexture(FruitVisualRepresentation visualRepresentation)

View File

@ -126,8 +126,7 @@ namespace osu.Game.Rulesets.Catch.UI
private float hyperDashTargetPosition; private float hyperDashTargetPosition;
private Bindable<bool> hitLighting; private Bindable<bool> hitLighting;
private readonly DrawablePool<HitExplosion> hitExplosionPool; private readonly HitExplosionContainer hitExplosionContainer;
private readonly Container<HitExplosion> hitExplosionContainer;
private readonly DrawablePool<CaughtFruit> caughtFruitPool; private readonly DrawablePool<CaughtFruit> caughtFruitPool;
private readonly DrawablePool<CaughtBanana> caughtBananaPool; private readonly DrawablePool<CaughtBanana> caughtBananaPool;
@ -148,7 +147,6 @@ namespace osu.Game.Rulesets.Catch.UI
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
hitExplosionPool = new DrawablePool<HitExplosion>(10),
caughtFruitPool = new DrawablePool<CaughtFruit>(50), caughtFruitPool = new DrawablePool<CaughtFruit>(50),
caughtBananaPool = new DrawablePool<CaughtBanana>(100), caughtBananaPool = new DrawablePool<CaughtBanana>(100),
// less capacity is needed compared to fruit because droplet is not stacked // 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, Anchor = Anchor.TopCentre,
Alpha = 0, Alpha = 0,
}, },
hitExplosionContainer = new Container<HitExplosion> hitExplosionContainer = new HitExplosionContainer
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
@ -297,7 +295,6 @@ namespace osu.Game.Rulesets.Catch.UI
caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject); droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject);
hitExplosionContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
} }
/// <summary> /// <summary>
@ -508,15 +505,8 @@ namespace osu.Game.Rulesets.Catch.UI
return position; return position;
} }
private void addLighting(CatchHitObject hitObject, float x, Color4 colour) private void addLighting(CatchHitObject hitObject, float x, Color4 colour) =>
{ hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, x, hitObject.Scale, colour, hitObject.RandomSeed));
HitExplosion hitExplosion = hitExplosionPool.Get();
hitExplosion.HitObject = hitObject;
hitExplosion.X = x;
hitExplosion.Scale = new Vector2(hitObject.Scale);
hitExplosion.ObjectColour = colour;
hitExplosionContainer.Add(hitExplosion);
}
private CaughtObject getCaughtObject(PalpableCatchHitObject source) private CaughtObject getCaughtObject(PalpableCatchHitObject source)
{ {

View File

@ -5,31 +5,16 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Objects.Pooling;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI namespace osu.Game.Rulesets.Catch.UI
{ {
public class HitExplosion : PoolableDrawable public class HitExplosion : PoolableDrawableWithLifetime<HitExplosionEntry>
{ {
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 largeFaint;
private readonly CircularContainer smallFaint; private readonly CircularContainer smallFaint;
private readonly CircularContainer directionalGlow1; 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; const double duration = 400;
@ -96,14 +91,13 @@ namespace osu.Game.Rulesets.Catch.UI
.FadeOut(duration * 2); .FadeOut(duration * 2);
const float angle_variangle = 15; // should be less than 45 const float angle_variangle = 15; // should be less than 45
directionalGlow1.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle); directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4);
directionalGlow2.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle); directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5);
this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out); this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out).Expire();
Expire(true);
} }
private void onColourChanged() private void setColour(Color4 objectColour)
{ {
const float roundness = 100; const float roundness = 100;

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects.Pooling;
namespace osu.Game.Rulesets.Catch.UI
{
public class HitExplosionContainer : PooledDrawableWithLifetimeContainer<HitExplosionEntry, HitExplosion>
{
protected override bool RemoveRewoundEntry => true;
private readonly DrawablePool<HitExplosion> pool;
public HitExplosionContainer()
{
AddInternal(pool = new DrawablePool<HitExplosion>(10));
}
protected override HitExplosion GetDrawable(HitExplosionEntry entry) => pool.Get(d => d.Apply(entry));
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.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;
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Pooling;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
@ -12,34 +13,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
/// <summary> /// <summary>
/// Visualises the <see cref="FollowPoint"/>s between two <see cref="DrawableOsuHitObject"/>s. /// Visualises the <see cref="FollowPoint"/>s between two <see cref="DrawableOsuHitObject"/>s.
/// </summary> /// </summary>
public class FollowPointConnection : PoolableDrawable public class FollowPointConnection : PoolableDrawableWithLifetime<FollowPointLifetimeEntry>
{ {
// Todo: These shouldn't be constants // Todo: These shouldn't be constants
public const int SPACING = 32; public const int SPACING = 32;
public const double PREEMPT = 800; public const double PREEMPT = 800;
public FollowPointLifetimeEntry Entry;
public DrawablePool<FollowPoint> Pool; public DrawablePool<FollowPoint> Pool;
protected override void PrepareForUse() protected override void OnApply(FollowPointLifetimeEntry entry)
{ {
base.PrepareForUse(); base.OnApply(entry);
Entry.Invalidated += onEntryInvalidated;
entry.Invalidated += onEntryInvalidated;
refreshPoints(); refreshPoints();
} }
protected override void FreeAfterUse() protected override void OnFree(FollowPointLifetimeEntry entry)
{ {
base.FreeAfterUse(); base.OnFree(entry);
Entry.Invalidated -= onEntryInvalidated;
entry.Invalidated -= onEntryInvalidated;
// Return points to the pool. // Return points to the pool.
ClearInternal(false); ClearInternal(false);
Entry = null;
} }
private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints); private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints);
@ -48,8 +44,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
ClearInternal(false); ClearInternal(false);
OsuHitObject start = Entry.Start; var entry = Entry;
OsuHitObject end = Entry.End; if (entry?.End == null) return;
OsuHitObject start = entry.Start;
OsuHitObject end = entry.End;
double startTime = start.GetEndTime(); double startTime = start.GetEndTime();
@ -87,14 +86,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
fp.FadeIn(end.TimeFadeIn); fp.FadeIn(end.TimeFadeIn);
fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out); fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out);
fp.MoveTo(pointEndPosition, 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;
} }
/// <summary> /// <summary>

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Performance;
@ -11,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
public class FollowPointLifetimeEntry : LifetimeEntry public class FollowPointLifetimeEntry : LifetimeEntry
{ {
public event Action Invalidated; public event Action? Invalidated;
public readonly OsuHitObject Start; public readonly OsuHitObject Start;
public FollowPointLifetimeEntry(OsuHitObject start) public FollowPointLifetimeEntry(OsuHitObject start)
@ -22,9 +24,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
bindEvents(); bindEvents();
} }
private OsuHitObject end; private OsuHitObject? end;
public OsuHitObject End public OsuHitObject? End
{ {
get => end; get => end;
set set
@ -56,11 +58,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
public void UnbindEvents() public void UnbindEvents()
{ {
if (Start != null) Start.DefaultsApplied -= onDefaultsApplied;
{ Start.PositionBindable.ValueChanged -= onPositionChanged;
Start.DefaultsApplied -= onDefaultsApplied;
Start.PositionBindable.ValueChanged -= onPositionChanged;
}
if (End != null) if (End != null)
{ {

View File

@ -6,43 +6,32 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Performance;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Pooling;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
/// <summary> /// <summary>
/// Visualises connections between <see cref="DrawableOsuHitObject"/>s. /// Visualises connections between <see cref="DrawableOsuHitObject"/>s.
/// </summary> /// </summary>
public class FollowPointRenderer : CompositeDrawable public class FollowPointRenderer : PooledDrawableWithLifetimeContainer<FollowPointLifetimeEntry, FollowPointConnection>
{ {
public override bool RemoveCompletedTransforms => false; public new IReadOnlyList<FollowPointLifetimeEntry> Entries => lifetimeEntries;
public IReadOnlyList<FollowPointLifetimeEntry> Entries => lifetimeEntries;
private DrawablePool<FollowPointConnection> connectionPool; private DrawablePool<FollowPointConnection> connectionPool;
private DrawablePool<FollowPoint> pointPool; private DrawablePool<FollowPoint> pointPool;
private readonly List<FollowPointLifetimeEntry> lifetimeEntries = new List<FollowPointLifetimeEntry>(); private readonly List<FollowPointLifetimeEntry> lifetimeEntries = new List<FollowPointLifetimeEntry>();
private readonly Dictionary<LifetimeEntry, FollowPointConnection> connectionsInUse = new Dictionary<LifetimeEntry, FollowPointConnection>();
private readonly Dictionary<HitObject, IBindable> startTimeMap = new Dictionary<HitObject, IBindable>(); private readonly Dictionary<HitObject, IBindable> startTimeMap = new Dictionary<HitObject, IBindable>();
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
public FollowPointRenderer()
{
lifetimeManager.EntryBecameAlive += onEntryBecameAlive;
lifetimeManager.EntryBecameDead += onEntryBecameDead;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
connectionPool = new DrawablePoolNoLifetime<FollowPointConnection>(1, 200), connectionPool = new DrawablePool<FollowPointConnection>(1, 200),
pointPool = new DrawablePoolNoLifetime<FollowPoint>(50, 1000) pointPool = new DrawablePool<FollowPoint>(50, 1000)
}; };
} }
@ -107,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
previousEntry.End = newEntry.Start; previousEntry.End = newEntry.Start;
} }
lifetimeManager.AddEntry(newEntry); Add(newEntry);
} }
private void removeEntry(OsuHitObject hitObject) private void removeEntry(OsuHitObject hitObject)
@ -118,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
entry.UnbindEvents(); entry.UnbindEvents();
lifetimeEntries.RemoveAt(index); lifetimeEntries.RemoveAt(index);
lifetimeManager.RemoveEntry(entry); Remove(entry);
if (index > 0) 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(); var connection = connectionPool.Get();
anyAliveChanged |= lifetimeManager.Update(Time.Current); connection.Pool = pointPool;
return anyAliveChanged; connection.Apply(entry);
} return connection;
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);
} }
private void onStartTimeChanged(OsuHitObject hitObject) private void onStartTimeChanged(OsuHitObject hitObject)
@ -171,16 +142,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
entry.UnbindEvents(); entry.UnbindEvents();
lifetimeEntries.Clear(); lifetimeEntries.Clear();
} }
private class DrawablePoolNoLifetime<T> : DrawablePool<T>
where T : PoolableDrawable, new()
{
public override bool RemoveWhenNotAlive => false;
public DrawablePoolNoLifetime(int initialSize, int? maximumSize = null)
: base(initialSize, maximumSize)
{
}
}
} }
} }

View File

@ -152,7 +152,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
while (Math.Abs(aimRotation - Arrow.Rotation) > 180) while (Math.Abs(aimRotation - Arrow.Rotation) > 180)
aimRotation += aimRotation < Arrow.Rotation ? 360 : -360; 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; Arrow.Rotation = aimRotation;
hasRotation = true; hasRotation = true;

View File

@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
@ -11,6 +8,11 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
internal class DrumRollHitContainer : ScrollingHitObjectContainer 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() protected override void Update()
{ {
base.Update(); base.Update();
@ -23,14 +25,5 @@ namespace osu.Game.Rulesets.Taiko.UI
Remove(flyingHit); 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);
}
} }
} }

View File

@ -0,0 +1,90 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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<Graphics.Backgrounds.Background>().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,
});
}
}

View File

@ -4,14 +4,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -29,15 +30,22 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneHitErrorMeter : OsuTestScene public class TestSceneHitErrorMeter : OsuTestScene
{ {
[Cached] [Cached(typeof(ScoreProcessor))]
private ScoreProcessor scoreProcessor = new ScoreProcessor(); private TestScoreProcessor scoreProcessor = new TestScoreProcessor();
[Cached(typeof(DrawableRuleset))] [Cached(typeof(DrawableRuleset))]
private TestDrawableRuleset drawableRuleset = new TestDrawableRuleset(); 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); 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); AddRepeatStep("New max positive", () => newJudgement(drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20);
AddStep("New fixed judgement (50ms)", () => newJudgement(50)); AddStep("New fixed judgement (50ms)", () => newJudgement(50));
ScheduledDelegate del = null;
AddStep("Judgement barrage", () => AddStep("Judgement barrage", () =>
{ {
int runCount = 0; int runCount = 0;
ScheduledDelegate del = null;
del = Scheduler.AddDelayed(() => del = Scheduler.AddDelayed(() =>
{ {
newJudgement(runCount++ / 10f); newJudgement(runCount++ / 10f);
@ -60,6 +67,7 @@ namespace osu.Game.Tests.Visual.Gameplay
del?.Cancel(); del?.Cancel();
}, 10, true); }, 10, true);
}); });
AddUntilStep("wait for barrage", () => del.Cancelled);
} }
[Test] [Test]
@ -84,10 +92,21 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
[Test] [Test]
public void TestCatch() public void TestEmpty()
{ {
AddStep("OD 1", () => recreateDisplay(new CatchHitWindows(), 1)); AddStep("empty windows", () => recreateDisplay(HitWindows.Empty, 5));
AddStep("OD 10", () => recreateDisplay(new CatchHitWindows(), 10));
AddStep("hit", () => newJudgement());
AddAssert("no bars added", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("circle added", () =>
this.ChildrenOfType<ColourHitErrorMeter>().All(
meter => meter.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Count() == 1));
AddStep("miss", () => newJudgement(50, HitResult.Miss));
AddAssert("no bars added", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("circle added", () =>
this.ChildrenOfType<ColourHitErrorMeter>().All(
meter => meter.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Count() == 2));
} }
private void recreateDisplay(HitWindows hitWindows, float overallDifficulty) 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()) scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = drawableRuleset.HitWindows }, new Judgement())
{ {
TimeOffset = offset == 0 ? RNG.Next(-150, 150) : offset, 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 Overlays { get; }
public override Container FrameStableComponents { get; } public override Container FrameStableComponents { get; }
public override IFrameStableClock FrameStableClock { get; } public override IFrameStableClock FrameStableClock { get; }
internal override bool FrameStablePlayback { get; set; }
public override IReadOnlyList<Mod> Mods { get; } public override IReadOnlyList<Mod> Mods { get; }
public override double GameplayStartTime { get; } public override double GameplayStartTime { get; }
@ -198,5 +218,10 @@ namespace osu.Game.Tests.Visual.Gameplay
public override void CancelResume() => throw new NotImplementedException(); public override void CancelResume() => throw new NotImplementedException();
} }
private class TestScoreProcessor : ScoreProcessor
{
public void Reset() => base.Reset(false);
}
} }
} }

View File

@ -76,9 +76,9 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectator); AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectator);
start(); start();
sendFrames();
waitForPlayer(); waitForPlayer();
sendFrames();
AddAssert("ensure frames arrived", () => replayHandler.HasFrames); AddAssert("ensure frames arrived", () => replayHandler.HasFrames);
AddUntilStep("wait for frame starvation", () => replayHandler.WaitingForFrame); AddUntilStep("wait for frame starvation", () => replayHandler.WaitingForFrame);
@ -116,12 +116,11 @@ namespace osu.Game.Tests.Visual.Gameplay
start(); start();
loadSpectatingScreen(); loadSpectatingScreen();
waitForPlayer();
AddStep("advance frame count", () => nextFrame = 300); AddStep("advance frame count", () => nextFrame = 300);
sendFrames(); sendFrames();
waitForPlayer();
AddUntilStep("playing from correct point in time", () => player.ChildrenOfType<DrawableRuleset>().First().FrameStableClock.CurrentTime > 30000); AddUntilStep("playing from correct point in time", () => player.ChildrenOfType<DrawableRuleset>().First().FrameStableClock.CurrentTime > 30000);
} }
@ -210,7 +209,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private double currentFrameStableTime private double currentFrameStableTime
=> player.ChildrenOfType<FrameStabilityContainer>().First().FrameStableClock.CurrentTime; => player.ChildrenOfType<FrameStabilityContainer>().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)); private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId));

View File

@ -264,14 +264,18 @@ namespace osu.Game.Collections
using (var sw = new SerializationWriter(storage.GetStream(database_name, FileAccess.Write))) using (var sw = new SerializationWriter(storage.GetStream(database_name, FileAccess.Write)))
{ {
sw.Write(database_version); 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.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); sw.Write(b.MD5Hash);
} }
} }

View File

@ -1,11 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum BackgroundSource public enum BackgroundSource
{ {
Skin, Skin,
Beatmap Beatmap,
[Description("Beatmap (with storyboard / video)")]
BeatmapWithStoryboard,
} }
} }

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.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);
}
}
}

View File

@ -30,9 +30,12 @@ namespace osu.Game.Graphics.Containers.Markdown
break; break;
case ListItemBlock listItemBlock: case ListItemBlock listItemBlock:
var isOrdered = ((ListBlock)listItemBlock.Parent).IsOrdered; bool isOrdered = ((ListBlock)listItemBlock.Parent)?.IsOrdered == true;
var childContainer = CreateListItem(listItemBlock, level, isOrdered);
OsuMarkdownListItem childContainer = CreateListItem(listItemBlock, level, isOrdered);
container.Add(childContainer); container.Add(childContainer);
foreach (var single in listItemBlock) foreach (var single in listItemBlock)
base.AddMarkdownComponent(single, childContainer.Content, level); base.AddMarkdownComponent(single, childContainer.Content, level);
break; break;

View File

@ -172,6 +172,8 @@ namespace osu.Game.Graphics.Containers
private class ScalingBackgroundScreen : BackgroundScreenDefault private class ScalingBackgroundScreen : BackgroundScreenDefault
{ {
protected override bool AllowStoryboardBackground => false;
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
{ {
this.FadeInFromZero(4000, Easing.OutQuint); this.FadeInFromZero(4000, Easing.OutQuint);

View File

@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Wiki.Markdown
case ParagraphBlock paragraphBlock: case ParagraphBlock paragraphBlock:
// Check if paragraph only contains an image // 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)); container.Add(new WikiMarkdownImageBlock(linkInline));
return; return;

View File

@ -0,0 +1,163 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System.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
{
/// <summary>
/// A container of <typeparamref name="TDrawable"/>s dynamically added/removed by model <typeparamref name="TEntry"/>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.
/// </summary>
/// <typeparam name="TEntry">The type of entries managed by this container.</typeparam>
/// <typeparam name="TDrawable">The type of drawables corresponding to the entries.</typeparam>
public abstract class PooledDrawableWithLifetimeContainer<TEntry, TDrawable> : CompositeDrawable
where TEntry : LifetimeEntry
where TDrawable : Drawable
{
/// <summary>
/// All entries added to this container, including dead entries.
/// </summary>
/// <remarks>
/// The enumeration order is undefined.
/// </remarks>
public IEnumerable<TEntry> Entries => allEntries;
/// <summary>
/// All alive entries and drawables corresponding to the entries.
/// </summary>
/// <remarks>
/// The enumeration order is undefined.
/// </remarks>
public IEnumerable<(TEntry Entry, TDrawable Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value));
/// <summary>
/// Whether to remove an entry when clock goes backward and crossed its <see cref="LifetimeEntry.LifetimeStart"/>.
/// Used when entries are dynamically added at its <see cref="LifetimeEntry.LifetimeStart"/> to prevent duplicated entries.
/// </summary>
protected virtual bool RemoveRewoundEntry => false;
/// <summary>
/// The amount of time prior to the current time within which entries should be considered alive.
/// </summary>
internal double PastLifetimeExtension { get; set; }
/// <summary>
/// The amount of time after the current time within which entries should be considered alive.
/// </summary>
internal double FutureLifetimeExtension { get; set; }
private readonly Dictionary<TEntry, TDrawable> aliveDrawableMap = new Dictionary<TEntry, TDrawable>();
private readonly HashSet<TEntry> allEntries = new HashSet<TEntry>();
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
protected PooledDrawableWithLifetimeContainer()
{
lifetimeManager.EntryBecameAlive += entryBecameAlive;
lifetimeManager.EntryBecameDead += entryBecameDead;
lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary;
}
/// <summary>
/// Add a <typeparamref name="TEntry"/> to be managed by this container.
/// </summary>
/// <remarks>
/// The aliveness of the entry is not updated until <see cref="CheckChildrenLife"/>.
/// </remarks>
public virtual void Add(TEntry entry)
{
allEntries.Add(entry);
lifetimeManager.AddEntry(entry);
}
/// <summary>
/// Remove a <typeparamref name="TEntry"/> from this container.
/// </summary>
/// <remarks>
/// If the entry was alive, the corresponding drawable is removed.
/// </remarks>
/// <returns>Whether the entry was in this container.</returns>
public virtual bool Remove(TEntry entry)
{
if (!lifetimeManager.RemoveEntry(entry)) return false;
allEntries.Remove(entry);
return true;
}
/// <summary>
/// Initialize new <typeparamref name="TDrawable"/> corresponding <paramref name="entry"/>.
/// </summary>
/// <returns>The <typeparamref name="TDrawable"/> corresponding to the entry.</returns>
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);
}
/// <summary>
/// Add a <typeparamref name="TDrawable"/> corresponding to <paramref name="entry"/> to this container.
/// </summary>
/// <remarks>
/// Invoked when the entry became alive and a <typeparamref name="TDrawable"/> is obtained by <see cref="GetDrawable"/>.
/// </remarks>
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);
}
/// <summary>
/// Remove a <typeparamref name="TDrawable"/> corresponding to <paramref name="entry"/> from this container.
/// </summary>
/// <remarks>
/// Invoked when the entry became dead.
/// </remarks>
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);
}
/// <summary>
/// Remove all <typeparamref name="TEntry"/>s.
/// </summary>
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;
}
}
}

View File

@ -68,10 +68,7 @@ namespace osu.Game.Rulesets.UI
private bool frameStablePlayback = true; private bool frameStablePlayback = true;
/// <summary> internal override bool FrameStablePlayback
/// Whether to enable frame-stable playback.
/// </summary>
internal bool FrameStablePlayback
{ {
get => frameStablePlayback; get => frameStablePlayback;
set set
@ -431,6 +428,11 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
public abstract IFrameStableClock FrameStableClock { get; } public abstract IFrameStableClock FrameStableClock { get; }
/// <summary>
/// Whether to enable frame-stable playback.
/// </summary>
internal abstract bool FrameStablePlayback { get; set; }
/// <summary> /// <summary>
/// The mods which are to be applied. /// The mods which are to be applied.
/// </summary> /// </summary>

View File

@ -3,35 +3,23 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Pooling;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
public class HitObjectContainer : CompositeDrawable, IHitObjectContainer public class HitObjectContainer : PooledDrawableWithLifetimeContainer<HitObjectLifetimeEntry, DrawableHitObject>, IHitObjectContainer
{ {
/// <summary>
/// All entries in this <see cref="HitObjectContainer"/> including dead entries.
/// </summary>
public IEnumerable<HitObjectLifetimeEntry> Entries => allEntries;
/// <summary>
/// All alive entries and <see cref="DrawableHitObject"/>s used by the entries.
/// </summary>
public IEnumerable<(HitObjectLifetimeEntry Entry, DrawableHitObject Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value));
public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime); public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
public IEnumerable<DrawableHitObject> AliveObjects => AliveInternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime); public IEnumerable<DrawableHitObject> AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime);
/// <summary> /// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is judged. /// Invoked when a <see cref="DrawableHitObject"/> is judged.
@ -59,34 +47,16 @@ namespace osu.Game.Rulesets.UI
/// </remarks> /// </remarks>
internal event Action<HitObject> HitObjectUsageFinished; internal event Action<HitObject> HitObjectUsageFinished;
/// <summary>
/// The amount of time prior to the current time within which <see cref="HitObject"/>s should be considered alive.
/// </summary>
internal double PastLifetimeExtension { get; set; }
/// <summary>
/// The amount of time after the current time within which <see cref="HitObject"/>s should be considered alive.
/// </summary>
internal double FutureLifetimeExtension { get; set; }
private readonly Dictionary<DrawableHitObject, IBindable> startTimeMap = new Dictionary<DrawableHitObject, IBindable>(); private readonly Dictionary<DrawableHitObject, IBindable> startTimeMap = new Dictionary<DrawableHitObject, IBindable>();
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> aliveDrawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> nonPooledDrawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>(); private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> nonPooledDrawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
private readonly HashSet<HitObjectLifetimeEntry> allEntries = new HashSet<HitObjectLifetimeEntry>();
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IPooledHitObjectProvider pooledObjectProvider { get; set; } private IPooledHitObjectProvider pooledObjectProvider { get; set; }
public HitObjectContainer() public HitObjectContainer()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
lifetimeManager.EntryBecameAlive += entryBecameAlive;
lifetimeManager.EntryBecameDead += entryBecameDead;
lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary;
} }
protected override void LoadAsyncComplete() protected override void LoadAsyncComplete()
@ -99,63 +69,41 @@ namespace osu.Game.Rulesets.UI
#region Pooling support #region Pooling support
public void Add(HitObjectLifetimeEntry entry) public override bool Remove(HitObjectLifetimeEntry entry)
{ {
allEntries.Add(entry); if (!base.Remove(entry)) return false;
lifetimeManager.AddEntry(entry);
}
public bool Remove(HitObjectLifetimeEntry entry)
{
if (!lifetimeManager.RemoveEntry(entry)) return false;
// This logic is not in `Remove(DrawableHitObject)` because a non-pooled drawable may be removed by specifying its entry. // 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)) if (nonPooledDrawableMap.Remove(entry, out var drawable))
removeDrawable(drawable); removeDrawable(drawable);
allEntries.Remove(entry);
return true; return true;
} }
private void entryBecameAlive(LifetimeEntry lifetimeEntry) protected sealed override DrawableHitObject GetDrawable(HitObjectLifetimeEntry entry)
{ {
var entry = (HitObjectLifetimeEntry)lifetimeEntry; if (nonPooledDrawableMap.TryGetValue(entry, out var drawable))
Debug.Assert(!aliveDrawableMap.ContainsKey(entry)); return drawable;
bool isPooled = !nonPooledDrawableMap.TryGetValue(entry, out var drawable); return pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null) ??
drawable ??= pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null); throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}.");
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);
} }
private void entryBecameDead(LifetimeEntry lifetimeEntry) protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
{ {
var entry = (HitObjectLifetimeEntry)lifetimeEntry; if (nonPooledDrawableMap.ContainsKey(entry)) return;
Debug.Assert(aliveDrawableMap.ContainsKey(entry));
var drawable = aliveDrawableMap[entry]; addDrawable(drawable);
bool isPooled = !nonPooledDrawableMap.ContainsKey(entry); HitObjectUsageBegan?.Invoke(entry.HitObject);
}
protected override void RemoveDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
{
drawable.OnKilled(); drawable.OnKilled();
aliveDrawableMap.Remove(entry); if (nonPooledDrawableMap.ContainsKey(entry)) return;
if (isPooled) removeDrawable(drawable);
{ HitObjectUsageFinished?.Invoke(entry.HitObject);
removeDrawable(drawable);
HitObjectUsageFinished?.Invoke(entry.HitObject);
}
OnRemove(drawable);
} }
private void addDrawable(DrawableHitObject drawable) private void addDrawable(DrawableHitObject drawable)
@ -201,49 +149,8 @@ namespace osu.Game.Rulesets.UI
public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject); 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 #endregion
/// <summary>
/// Invoked after a <see cref="DrawableHitObject"/> is added to this container.
/// </summary>
protected virtual void OnAdd(DrawableHitObject drawableHitObject)
{
Debug.Assert(drawableHitObject.LoadState >= LoadState.Ready);
}
/// <summary>
/// Invoked after a <see cref="DrawableHitObject"/> is removed from this container.
/// </summary>
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 onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r);
private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r); private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r);

View File

@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK; using osuTK;
@ -45,13 +47,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
timeRange.ValueChanged += _ => layoutCache.Invalidate(); timeRange.ValueChanged += _ => layoutCache.Invalidate();
} }
public override void Clear()
{
base.Clear();
layoutComputed.Clear();
}
/// <summary> /// <summary>
/// Given a position in screen space, return the time within this column. /// Given a position in screen space, return the time within this column.
/// </summary> /// </summary>
@ -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); base.AddDrawable(entry, drawable);
drawableHitObject.DefaultsApplied += invalidateHitObject;
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) private void invalidateHitObject(DrawableHitObject hitObject)
@ -206,6 +204,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject) private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
{ {
// Origin position may be relative to the parent size
Debug.Assert(hitObject.Parent != null);
float originAdjustment = 0.0f; float originAdjustment = 0.0f;
// calculate the dimension of the part of the hitobject that should already be visible // calculate the dimension of the part of the hitobject that should already be visible

View File

@ -5,8 +5,8 @@ using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
@ -31,6 +31,8 @@ namespace osu.Game.Screens.Backgrounds
[Resolved] [Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } private IBindable<WorkingBeatmap> beatmap { get; set; }
protected virtual bool AllowStoryboardBackground => true;
public BackgroundScreenDefault(bool animateOnEnter = true) public BackgroundScreenDefault(bool animateOnEnter = true)
: base(animateOnEnter) : base(animateOnEnter)
{ {
@ -51,14 +53,41 @@ namespace osu.Game.Screens.Backgrounds
mode.ValueChanged += _ => Next(); mode.ValueChanged += _ => Next();
beatmap.ValueChanged += _ => Next(); beatmap.ValueChanged += _ => Next();
introSequence.ValueChanged += _ => Next(); introSequence.ValueChanged += _ => Next();
seasonalBackgroundLoader.SeasonalBackgroundChanged += Next; seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Next();
currentDisplay = RNG.Next(0, background_count); currentDisplay = RNG.Next(0, background_count);
Next(); Next();
} }
private void display(Background newBackground) private ScheduledDelegate nextTask;
private CancellationTokenSource cancellationTokenSource;
/// <summary>
/// Request loading the next background.
/// </summary>
/// <returns>Whether a new background was queued for load. May return false if the current background is still valid.</returns>
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?.FadeOut(800, Easing.InOutSine);
background?.Expire(); background?.Expire();
@ -67,62 +96,51 @@ namespace osu.Game.Screens.Backgrounds
currentDisplay++; 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() private Background createBackground()
{ {
Background newBackground; // seasonal background loading gets highest priority.
string backgroundName; Background newBackground = seasonalBackgroundLoader.LoadNextBackground();
var seasonalBackground = seasonalBackgroundLoader.LoadNextBackground(); if (newBackground == null && user.Value?.IsSupporter == true)
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)
{ {
switch (mode.Value) switch (mode.Value)
{ {
case BackgroundSource.Beatmap: case BackgroundSource.Beatmap:
newBackground = new BeatmapBackground(beatmap.Value, backgroundName); case BackgroundSource.BeatmapWithStoryboard:
break; {
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; break;
}
} }
} }
else
newBackground = new Background(backgroundName);
newBackground ??= new Background(getBackgroundTextureName());
newBackground.Depth = currentDisplay; newBackground.Depth = currentDisplay;
return newBackground; 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 class SkinnedBackground : Background
{ {
private readonly Skin skin; private readonly Skin skin;

View File

@ -214,7 +214,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
protected override void OnNewJudgement(JudgementResult judgement) protected override void OnNewJudgement(JudgementResult judgement)
{ {
if (!judgement.IsHit) if (!judgement.IsHit || judgement.HitObject.HitWindows?.WindowFor(HitResult.Miss) == 0)
return; return;
if (judgementsContainer.Count > max_concurrent_judgements) 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 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; private const int judgement_fade_duration = 5000;

View File

@ -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; } public bool IsRemoved { get; private set; }

View File

@ -32,15 +32,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{ {
base.LoadComplete(); base.LoadComplete();
processor.NewJudgement += onNewJudgement; processor.NewJudgement += OnNewJudgement;
}
private void onNewJudgement(JudgementResult result)
{
if (result.HitObject.HitWindows?.WindowFor(HitResult.Miss) == 0)
return;
OnNewJudgement(result);
} }
protected abstract void OnNewJudgement(JudgementResult judgement); protected abstract void OnNewJudgement(JudgementResult judgement);
@ -74,7 +66,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
base.Dispose(isDisposing); base.Dispose(isDisposing);
if (processor != null) if (processor != null)
processor.NewJudgement -= onNewJudgement; processor.NewJudgement -= OnNewJudgement;
} }
} }
} }

View File

@ -100,7 +100,13 @@ namespace osu.Game.Screens.Play
{ {
// The source is stopped by a frequency fade first. // The source is stopped by a frequency fade first.
if (isPaused.NewValue) 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 else
this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In);
} }

View File

@ -81,10 +81,6 @@ namespace osu.Game.Screens.Play
[Resolved] [Resolved]
private ScoreManager scoreManager { get; set; } private ScoreManager scoreManager { get; set; }
private RulesetInfo rulesetInfo;
private Ruleset ruleset;
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
@ -94,6 +90,10 @@ namespace osu.Game.Screens.Play
[Resolved] [Resolved]
private SpectatorClient spectatorClient { get; set; } private SpectatorClient spectatorClient { get; set; }
protected Ruleset GameplayRuleset { get; private set; }
protected GameplayBeatmap GameplayBeatmap { get; private set; }
private Sample sampleRestart; private Sample sampleRestart;
public BreakOverlay BreakOverlay; public BreakOverlay BreakOverlay;
@ -144,8 +144,6 @@ namespace osu.Game.Screens.Play
Configuration = configuration ?? new PlayerConfiguration(); Configuration = configuration ?? new PlayerConfiguration();
} }
protected GameplayBeatmap GameplayBeatmap { get; private set; }
private ScreenSuspensionHandler screenSuspension; private ScreenSuspensionHandler screenSuspension;
private DependencyContainer dependencies; private DependencyContainer dependencies;
@ -164,7 +162,7 @@ namespace osu.Game.Screens.Play
// ensure the score is in a consistent state with the current player. // ensure the score is in a consistent state with the current player.
Score.ScoreInfo.Beatmap = Beatmap.Value.BeatmapInfo; Score.ScoreInfo.Beatmap = Beatmap.Value.BeatmapInfo;
Score.ScoreInfo.Ruleset = rulesetInfo; Score.ScoreInfo.Ruleset = GameplayRuleset.RulesetInfo;
Score.ScoreInfo.Mods = Mods.Value.ToArray(); Score.ScoreInfo.Mods = Mods.Value.ToArray();
PrepareReplay(); PrepareReplay();
@ -211,16 +209,16 @@ namespace osu.Game.Screens.Play
if (game is OsuGame osuGame) if (game is OsuGame osuGame)
LocalUserPlaying.BindTo(osuGame.LocalUserPlaying); LocalUserPlaying.BindTo(osuGame.LocalUserPlaying);
DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); DrawableRuleset = GameplayRuleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value);
dependencies.CacheAs(DrawableRuleset); dependencies.CacheAs(DrawableRuleset);
ScoreProcessor = ruleset.CreateScoreProcessor(); ScoreProcessor = GameplayRuleset.CreateScoreProcessor();
ScoreProcessor.ApplyBeatmap(playableBeatmap); ScoreProcessor.ApplyBeatmap(playableBeatmap);
ScoreProcessor.Mods.BindTo(Mods); ScoreProcessor.Mods.BindTo(Mods);
dependencies.CacheAs(ScoreProcessor); dependencies.CacheAs(ScoreProcessor);
HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); HealthProcessor = GameplayRuleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime);
HealthProcessor.ApplyBeatmap(playableBeatmap); HealthProcessor.ApplyBeatmap(playableBeatmap);
dependencies.CacheAs(HealthProcessor); 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 // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
// full access to all skin sources. // 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. // load the skinning hierarchy first.
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
@ -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.) // 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. // 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. // 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))); GameplayClockContainer.Add(hudRulesetContainer.WithChild(createOverlayComponents(Beatmap.Value)));
@ -480,18 +478,18 @@ namespace osu.Game.Screens.Play
if (Beatmap.Value.Beatmap == null) if (Beatmap.Value.Beatmap == null)
throw new InvalidOperationException("Beatmap was not loaded"); throw new InvalidOperationException("Beatmap was not loaded");
rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset; var rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset;
ruleset = rulesetInfo.CreateInstance(); GameplayRuleset = rulesetInfo.CreateInstance();
try try
{ {
playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Mods.Value); playable = Beatmap.Value.GetPlayableBeatmap(GameplayRuleset.RulesetInfo, Mods.Value);
} }
catch (BeatmapInvalidForRulesetException) catch (BeatmapInvalidForRulesetException)
{ {
// A playable beatmap may not be creatable with the user's preferred ruleset, so try using the beatmap's default ruleset // 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; rulesetInfo = Beatmap.Value.BeatmapInfo.Ruleset;
ruleset = rulesetInfo.CreateInstance(); GameplayRuleset = rulesetInfo.CreateInstance();
playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, Mods.Value); playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, Mods.Value);
} }
@ -585,6 +583,29 @@ namespace osu.Game.Screens.Play
/// <param name="time">The destination time to seek to.</param> /// <param name="time">The destination time to seek to.</param>
public void Seek(double time) => GameplayClockContainer.Seek(time); public void Seek(double time) => GameplayClockContainer.Seek(time);
private ScheduledDelegate frameStablePlaybackResetDelegate;
/// <summary>
/// Seeks to a specific time in gameplay, bypassing frame stability.
/// </summary>
/// <remarks>
/// Intermediate hitobject judgements may not be applied or reverted correctly during this seek.
/// </remarks>
/// <param name="time">The destination time to seek to.</param>
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);
}
/// <summary> /// <summary>
/// Restart gameplay via a parent <see cref="PlayerLoader"/>. /// Restart gameplay via a parent <see cref="PlayerLoader"/>.
/// <remarks>This can be called from a child screen in order to trigger the restart process.</remarks> /// <remarks>This can be called from a child screen in order to trigger the restart process.</remarks>
@ -918,11 +939,10 @@ namespace osu.Game.Screens.Play
/// Creates the player's <see cref="Scoring.Score"/>. /// Creates the player's <see cref="Scoring.Score"/>.
/// </summary> /// </summary>
/// <returns>The <see cref="Scoring.Score"/>.</returns> /// <returns>The <see cref="Scoring.Score"/>.</returns>
protected virtual Score CreateScore() => protected virtual Score CreateScore() => new Score
new Score {
{ ScoreInfo = new ScoreInfo { User = api.LocalUser.Value },
ScoreInfo = new ScoreInfo { User = api.LocalUser.Value }, };
};
/// <summary> /// <summary>
/// Imports the player's <see cref="Scoring.Score"/> to the local database. /// Imports the player's <see cref="Scoring.Score"/> to the local database.

View File

@ -1,14 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -16,11 +16,11 @@ namespace osu.Game.Screens.Play
{ {
public class SpectatorPlayer : Player public class SpectatorPlayer : Player
{ {
private readonly Score score;
[Resolved] [Resolved]
private SpectatorClient spectatorClient { get; set; } private SpectatorClient spectatorClient { get; set; }
private readonly Score score;
protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap
public SpectatorPlayer(Score score) public SpectatorPlayer(Score score)
@ -28,11 +28,6 @@ namespace osu.Game.Screens.Play
this.score = score; this.score = score;
} }
protected override Score CreateScore() => score;
protected override ResultsScreen CreateResults(ScoreInfo score)
=> new SpectatorResultsScreen(score);
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() 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() protected override void PrepareReplay()
{ {
DrawableRuleset?.SetReplayScore(score); 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) public override bool OnExiting(IScreen next)
{ {
spectatorClient.OnUserBeganPlaying -= userBeganPlaying; spectatorClient.OnUserBeganPlaying -= userBeganPlaying;
spectatorClient.OnNewFrames -= userSentFrames;
return base.OnExiting(next); return base.OnExiting(next);
} }
@ -85,7 +121,10 @@ namespace osu.Game.Screens.Play
base.Dispose(isDisposing); base.Dispose(isDisposing);
if (spectatorClient != null) if (spectatorClient != null)
{
spectatorClient.OnUserBeganPlaying -= userBeganPlaying; spectatorClient.OnUserBeganPlaying -= userBeganPlaying;
spectatorClient.OnNewFrames -= userSentFrames;
}
} }
} }
} }

View File

@ -15,8 +15,6 @@ using osu.Game.Database;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
@ -71,8 +69,6 @@ namespace osu.Game.Screens.Spectate
playingUserStates.BindTo(spectatorClient.PlayingUserStates); playingUserStates.BindTo(spectatorClient.PlayingUserStates);
playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true);
spectatorClient.OnNewFrames += userSentFrames;
managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(beatmapUpdated); managerUpdated.BindValueChanged(beatmapUpdated);
@ -197,29 +193,6 @@ namespace osu.Game.Screens.Spectate
Schedule(() => StartGameplay(userId, gameplayState)); 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);
}
}
/// <summary> /// <summary>
/// Invoked when a spectated user's state has changed. /// Invoked when a spectated user's state has changed.
/// </summary> /// </summary>
@ -260,8 +233,6 @@ namespace osu.Game.Screens.Spectate
if (spectatorClient != null) if (spectatorClient != null)
{ {
spectatorClient.OnNewFrames -= userSentFrames;
foreach (var (userId, _) in userMap) foreach (var (userId, _) in userMap)
spectatorClient.StopWatchingUser(userId); spectatorClient.StopWatchingUser(userId);
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -70,22 +71,48 @@ namespace osu.Game.Skinning
updateSample(); 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) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin); base.SkinChanged(skin);
updateSample(); updateSample();
} }
/// <summary>
/// Whether this sample was playing before a skin source change.
/// </summary>
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() private void updateSample()
{ {
if (sampleInfo == null) if (sampleInfo == null)
return; return;
bool wasPlaying = Playing;
sampleContainer.Clear();
Sample = null;
var sample = CurrentSkin.GetSample(sampleInfo); var sample = CurrentSkin.GetSample(sampleInfo);
if (sample == null) 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 #region Re-expose AudioContainer
public BindableNumber<double> Volume => sampleContainer.Volume; public BindableNumber<double> Volume => sampleContainer.Volume;

View File

@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual
{ {
dummyAPI = new DummyAPIAccess(); dummyAPI = new DummyAPIAccess();
Dependencies.CacheAs<IAPIProvider>(dummyAPI); Dependencies.CacheAs<IAPIProvider>(dummyAPI);
Add(dummyAPI); base.Content.Add(dummyAPI);
} }
return Dependencies; return Dependencies;

View File

@ -75,5 +75,10 @@ namespace osu.Game.Utils
/// </param> /// </param>
public static float NextSingle(int seed, int series = 0) => public static float NextSingle(int seed, int series = 0) =>
(float)(NextULong(seed, series) & ((1 << 24) - 1)) / (1 << 24); // float has 24-bit precision (float)(NextULong(seed, series) & ((1 << 24) - 1)) / (1 << 24); // float has 24-bit precision
/// <summary>
/// Compute a random floating point value between <paramref name="min"/> and <paramref name="max"/> from given seed and series number.
/// </summary>
public static float NextSingle(float min, float max, int seed, int series = 0) => min + NextSingle(seed, series) * (max - min);
} }
} }

View File

@ -34,7 +34,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="ppy.osu.Framework" Version="2021.601.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.608.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.525.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.525.0" />
<PackageReference Include="Sentry" Version="3.4.0" /> <PackageReference Include="Sentry" Version="3.4.0" />
<PackageReference Include="SharpCompress" Version="0.28.2" /> <PackageReference Include="SharpCompress" Version="0.28.2" />

View File

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