Merge branch 'master' into more-ui-sfx

This commit is contained in:
Dean Herbert 2021-06-18 23:24:19 +09:00 committed by GitHub
commit b1fd812805
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 252 additions and 188 deletions

View File

@ -12,12 +12,10 @@ using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Skinning.Default namespace osu.Game.Rulesets.Catch.Skinning.Default
{ {
public class DefaultCatcher : CompositeDrawable, ICatcherSprite public class DefaultCatcher : CompositeDrawable
{ {
public Bindable<CatcherAnimationState> CurrentState { get; } = new Bindable<CatcherAnimationState>(); public Bindable<CatcherAnimationState> CurrentState { get; } = new Bindable<CatcherAnimationState>();
public Texture CurrentTexture => sprite.Texture;
private readonly Sprite sprite; private readonly Sprite sprite;
private readonly Dictionary<CatcherAnimationState, Texture> textures = new Dictionary<CatcherAnimationState, Texture>(); private readonly Dictionary<CatcherAnimationState, Texture> textures = new Dictionary<CatcherAnimationState, Texture>();

View File

@ -1,12 +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.Graphics.Textures;
namespace osu.Game.Rulesets.Catch.Skinning
{
public interface ICatcherSprite
{
Texture CurrentTexture { get; }
}
}

View File

@ -9,21 +9,17 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{ {
public class LegacyCatcherNew : CompositeDrawable, ICatcherSprite public class LegacyCatcherNew : CompositeDrawable
{ {
[Resolved] [Resolved]
private Bindable<CatcherAnimationState> currentState { get; set; } private Bindable<CatcherAnimationState> currentState { get; set; }
public Texture CurrentTexture => (currentDrawable as TextureAnimation)?.CurrentFrame ?? (currentDrawable as Sprite)?.Texture;
private readonly Dictionary<CatcherAnimationState, Drawable> drawables = new Dictionary<CatcherAnimationState, Drawable>(); private readonly Dictionary<CatcherAnimationState, Drawable> drawables = new Dictionary<CatcherAnimationState, Drawable>();
private Drawable currentDrawable; private Drawable currentDrawable;

View File

@ -3,19 +3,14 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{ {
public class LegacyCatcherOld : CompositeDrawable, ICatcherSprite public class LegacyCatcherOld : CompositeDrawable
{ {
public Texture CurrentTexture => (InternalChild as TextureAnimation)?.CurrentFrame ?? (InternalChild as Sprite)?.Texture;
public LegacyCatcherOld() public LegacyCatcherOld()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;

View File

@ -9,7 +9,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -17,7 +16,6 @@ using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Skinning; using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -83,18 +81,17 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
private readonly Container<CaughtObject> droppedObjectTarget; private readonly Container<CaughtObject> droppedObjectTarget;
[Cached] public CatcherAnimationState CurrentState
protected readonly Bindable<CatcherAnimationState> CurrentStateBindable = new Bindable<CatcherAnimationState>(); {
get => body.AnimationState.Value;
public CatcherAnimationState CurrentState => CurrentStateBindable.Value; private set => body.AnimationState.Value = value;
}
/// <summary> /// <summary>
/// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable. /// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
/// </summary> /// </summary>
public const float ALLOWED_CATCH_RANGE = 0.8f; public const float ALLOWED_CATCH_RANGE = 0.8f;
internal Texture CurrentTexture => ((ICatcherSprite)currentCatcher.Drawable).CurrentTexture;
private bool dashing; private bool dashing;
public bool Dashing public bool Dashing
@ -121,7 +118,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
private readonly float catchWidth; private readonly float catchWidth;
private readonly SkinnableDrawable currentCatcher; private readonly SkinnableCatcher body;
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR; private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR; private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
@ -161,13 +158,7 @@ namespace osu.Game.Rulesets.Catch.UI
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
}, },
currentCatcher = new SkinnableDrawable( body = new SkinnableCatcher(),
new CatchSkinComponent(CatchSkinComponents.Catcher),
_ => new DefaultCatcher())
{
Anchor = Anchor.TopCentre,
OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE
},
hitExplosionContainer = new HitExplosionContainer hitExplosionContainer = new HitExplosionContainer
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
@ -268,17 +259,16 @@ namespace osu.Game.Rulesets.Catch.UI
SetHyperDashState(); SetHyperDashState();
if (result.IsHit) if (result.IsHit)
updateState(hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
else if (!(hitObject is Banana)) else if (!(hitObject is Banana))
updateState(CatcherAnimationState.Fail); CurrentState = CatcherAnimationState.Fail;
} }
public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result) public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result)
{ {
var catchResult = (CatchJudgementResult)result; var catchResult = (CatchJudgementResult)result;
if (CurrentState != catchResult.CatcherAnimationState) CurrentState = catchResult.CatcherAnimationState;
updateState(catchResult.CatcherAnimationState);
if (HyperDashing != catchResult.CatcherHyperDash) if (HyperDashing != catchResult.CatcherHyperDash)
{ {
@ -373,14 +363,6 @@ namespace osu.Game.Rulesets.Catch.UI
} }
} }
private void updateState(CatcherAnimationState state)
{
if (CurrentState == state)
return;
CurrentStateBindable.Value = state;
}
private void placeCaughtObject(DrawablePalpableCatchHitObject drawableObject, Vector2 position) private void placeCaughtObject(DrawablePalpableCatchHitObject drawableObject, Vector2 position)
{ {
var caughtObject = getCaughtObject(drawableObject.HitObject); var caughtObject = getCaughtObject(drawableObject.HitObject);

View File

@ -0,0 +1,43 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Timing;
using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
/// <summary>
/// A trail of the catcher.
/// It also represents a hyper dash afterimage.
/// </summary>
public class CatcherTrail : PoolableDrawable
{
public CatcherAnimationState AnimationState
{
set => body.AnimationState.Value = value;
}
private readonly SkinnableCatcher body;
public CatcherTrail()
{
Size = new Vector2(CatcherArea.CATCHER_SIZE);
Origin = Anchor.TopCentre;
Blending = BlendingParameters.Additive;
InternalChild = body = new SkinnableCatcher
{
// Using a frozen clock because trails should not be animated when the skin has an animated catcher.
// TODO: The animation should be frozen at the animation frame at the time of the trail generation.
Clock = new FramedClock(new ManualClock()),
};
}
protected override void FreeAfterUse()
{
ClearTransforms();
base.FreeAfterUse();
}
}
}

View File

@ -19,11 +19,11 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
private readonly Catcher catcher; private readonly Catcher catcher;
private readonly DrawablePool<CatcherTrailSprite> trailPool; private readonly DrawablePool<CatcherTrail> trailPool;
private readonly Container<CatcherTrailSprite> dashTrails; private readonly Container<CatcherTrail> dashTrails;
private readonly Container<CatcherTrailSprite> hyperDashTrails; private readonly Container<CatcherTrail> hyperDashTrails;
private readonly Container<CatcherTrailSprite> endGlowSprites; private readonly Container<CatcherTrail> endGlowSprites;
private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR; private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
@ -83,10 +83,10 @@ namespace osu.Game.Rulesets.Catch.UI
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
trailPool = new DrawablePool<CatcherTrailSprite>(30), trailPool = new DrawablePool<CatcherTrail>(30),
dashTrails = new Container<CatcherTrailSprite> { RelativeSizeAxes = Axes.Both }, dashTrails = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both },
hyperDashTrails = new Container<CatcherTrailSprite> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR }, hyperDashTrails = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
endGlowSprites = new Container<CatcherTrailSprite> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR }, endGlowSprites = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
}; };
} }
@ -116,15 +116,12 @@ namespace osu.Game.Rulesets.Catch.UI
Scheduler.AddDelayed(displayTrail, catcher.HyperDashing ? 25 : 50); Scheduler.AddDelayed(displayTrail, catcher.HyperDashing ? 25 : 50);
} }
private CatcherTrailSprite createTrailSprite(Container<CatcherTrailSprite> target) private CatcherTrail createTrailSprite(Container<CatcherTrail> target)
{ {
CatcherTrailSprite sprite = trailPool.Get(); CatcherTrail sprite = trailPool.Get();
sprite.Texture = catcher.CurrentTexture; sprite.AnimationState = catcher.CurrentState;
sprite.Anchor = catcher.Anchor;
sprite.Scale = catcher.Scale; sprite.Scale = catcher.Scale;
sprite.Blending = BlendingParameters.Additive;
sprite.RelativePositionAxes = catcher.RelativePositionAxes;
sprite.Position = catcher.Position; sprite.Position = catcher.Position;
target.Add(sprite); target.Add(sprite);

View File

@ -1,40 +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.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
public class CatcherTrailSprite : PoolableDrawable
{
public Texture Texture
{
set => sprite.Texture = value;
}
private readonly Sprite sprite;
public CatcherTrailSprite()
{
InternalChild = sprite = new Sprite
{
RelativeSizeAxes = Axes.Both
};
Size = new Vector2(CatcherArea.CATCHER_SIZE);
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE;
}
protected override void FreeAfterUse()
{
ClearTransforms();
base.FreeAfterUse();
}
}
}

View File

@ -0,0 +1,33 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
/// <summary>
/// The visual representation of the <see cref="Catcher"/>.
/// It includes the body part of the catcher and the catcher plate.
/// </summary>
public class SkinnableCatcher : SkinnableDrawable
{
/// <summary>
/// This is used by skin elements to determine which texture of the catcher is used.
/// </summary>
[Cached]
public readonly Bindable<CatcherAnimationState> AnimationState = new Bindable<CatcherAnimationState>();
public SkinnableCatcher()
: base(new CatchSkinComponent(CatchSkinComponents.Catcher), _ => new DefaultCatcher())
{
Anchor = Anchor.TopCentre;
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE;
}
}
}

View File

@ -0,0 +1,12 @@
// 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.Osu.Mods
{
/// <summary>
/// Any mod which affects the animation or visibility of approach circles. Should be used for incompatibility purposes.
/// </summary>
public interface IMutateApproachCircles
{
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -11,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IMutateApproachCircles
{ {
public override string Name => "Approach Different"; public override string Name => "Approach Different";
public override string Acronym => "AD"; public override string Acronym => "AD";
@ -19,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)] [SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
public BindableFloat Scale { get; } = new BindableFloat(4) public BindableFloat Scale { get; } = new BindableFloat(4)
{ {

View File

@ -14,12 +14,12 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModHidden : ModHidden public class OsuModHidden : ModHidden, IMutateApproachCircles
{ {
public override string Description => @"Play with no approach circles and fading circles/sliders."; public override string Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => 1.06;
public override Type[] IncompatibleMods => new[] { typeof(OsuModTraceable), typeof(OsuModSpinIn) }; public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
private const double fade_in_duration_multiplier = 0.4; private const double fade_in_duration_multiplier = 0.4;
private const double fade_out_duration_multiplier = 0.3; private const double fade_out_duration_multiplier = 0.3;

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
/// <summary> /// <summary>
/// Adjusts the size of hit objects during their fade in animation. /// Adjusts the size of hit objects during their fade in animation.
/// </summary> /// </summary>
public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IMutateApproachCircles
{ {
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
protected virtual float EndScale => 1; protected virtual float EndScale => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn), typeof(OsuModTraceable) }; public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{ {

View File

@ -12,7 +12,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModSpinIn : ModWithVisibilityAdjustment public class OsuModSpinIn : ModWithVisibilityAdjustment, IMutateApproachCircles
{ {
public override string Name => "Spin In"; public override string Name => "Spin In";
public override string Acronym => "SI"; public override string Acronym => "SI";
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
// todo: this mod should be able to be compatible with hidden with a bit of further implementation. // todo: this mod should be able to be compatible with hidden with a bit of further implementation.
public override Type[] IncompatibleMods => new[] { typeof(OsuModObjectScaleTween), typeof(OsuModHidden), typeof(OsuModTraceable) }; public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
private const int rotate_offset = 360; private const int rotate_offset = 360;
private const float rotate_starting_width = 2; private const float rotate_starting_width = 2;

View File

@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModTraceable : ModWithVisibilityAdjustment public class OsuModTraceable : ModWithVisibilityAdjustment, IMutateApproachCircles
{ {
public override string Name => "Traceable"; public override string Name => "Traceable";
public override string Acronym => "TC"; public override string Acronym => "TC";
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Put your faith in the approach circles..."; public override string Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) }; public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{ {

View File

@ -21,6 +21,14 @@ namespace osu.Game.Tests.Mods
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object })); Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object }));
} }
[Test]
public void TestModIsCompatibleByItselfWithIncompatibleInterface()
{
var mod = new Mock<CustomMod1>();
mod.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object }));
}
[Test] [Test]
public void TestIncompatibleThroughTopLevel() public void TestIncompatibleThroughTopLevel()
{ {
@ -34,6 +42,20 @@ namespace osu.Game.Tests.Mods
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False); Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False);
} }
[Test]
public void TestIncompatibleThroughInterface()
{
var mod1 = new Mock<CustomMod1>();
var mod2 = new Mock<CustomMod2>();
mod1.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
mod2.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
// Test both orderings.
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, mod2.Object }), Is.False);
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False);
}
[Test] [Test]
public void TestMultiModIncompatibleWithTopLevel() public void TestMultiModIncompatibleWithTopLevel()
{ {
@ -149,11 +171,15 @@ namespace osu.Game.Tests.Mods
Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
} }
public abstract class CustomMod1 : Mod public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
{ {
} }
public abstract class CustomMod2 : Mod public abstract class CustomMod2 : Mod, IModCompatibilitySpecification
{
}
public interface IModCompatibilitySpecification
{ {
} }
} }

View File

@ -190,20 +190,29 @@ namespace osu.Game
AddFont(Resources, @"Fonts/osuFont"); AddFont(Resources, @"Fonts/osuFont");
AddFont(Resources, @"Fonts/Torus-Regular"); AddFont(Resources, @"Fonts/Torus/Torus-Regular");
AddFont(Resources, @"Fonts/Torus-Light"); AddFont(Resources, @"Fonts/Torus/Torus-Light");
AddFont(Resources, @"Fonts/Torus-SemiBold"); AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
AddFont(Resources, @"Fonts/Torus-Bold"); AddFont(Resources, @"Fonts/Torus/Torus-Bold");
AddFont(Resources, @"Fonts/Noto-Basic"); AddFont(Resources, @"Fonts/Inter/Inter-Regular");
AddFont(Resources, @"Fonts/Noto-Hangul"); AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
AddFont(Resources, @"Fonts/Noto-CJK-Basic"); AddFont(Resources, @"Fonts/Inter/Inter-Light");
AddFont(Resources, @"Fonts/Noto-CJK-Compatibility"); AddFont(Resources, @"Fonts/Inter/Inter-LightItalic");
AddFont(Resources, @"Fonts/Noto-Thai"); AddFont(Resources, @"Fonts/Inter/Inter-SemiBold");
AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic");
AddFont(Resources, @"Fonts/Inter/Inter-Bold");
AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
AddFont(Resources, @"Fonts/Venera-Light"); AddFont(Resources, @"Fonts/Noto/Noto-Basic");
AddFont(Resources, @"Fonts/Venera-Bold"); AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
AddFont(Resources, @"Fonts/Venera-Black"); AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility");
AddFont(Resources, @"Fonts/Noto/Noto-Thai");
AddFont(Resources, @"Fonts/Venera/Venera-Light");
AddFont(Resources, @"Fonts/Venera/Venera-Bold");
AddFont(Resources, @"Fonts/Venera/Venera-Black");
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY; Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;

View File

@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Mods
base.OnModSelected(mod); base.OnModSelected(mod);
foreach (var section in ModSectionsContainer.Children) foreach (var section in ModSectionsContainer.Children)
section.DeselectTypes(mod.IncompatibleMods, true); section.DeselectTypes(mod.IncompatibleMods, true, mod);
} }
} }
} }

View File

@ -159,12 +159,16 @@ namespace osu.Game.Overlays.Mods
/// </summary> /// </summary>
/// <param name="modTypes">The types of <see cref="Mod"/>s which should be deselected.</param> /// <param name="modTypes">The types of <see cref="Mod"/>s which should be deselected.</param>
/// <param name="immediate">Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow.</param> /// <param name="immediate">Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow.</param>
public void DeselectTypes(IEnumerable<Type> modTypes, bool immediate = false) /// <param name="newSelection">If this deselection is triggered by a user selection, this should contain the newly selected type. This type will never be deselected, even if it matches one provided in <paramref name="modTypes"/>.</param>
public void DeselectTypes(IEnumerable<Type> modTypes, bool immediate = false, Mod newSelection = null)
{ {
foreach (var button in Buttons) foreach (var button in Buttons)
{ {
if (button.SelectedMod == null) continue; if (button.SelectedMod == null) continue;
if (button.SelectedMod == newSelection)
continue;
foreach (var type in modTypes) foreach (var type in modTypes)
{ {
if (type.IsInstanceOfType(button.SelectedMod)) if (type.IsInstanceOfType(button.SelectedMod))

View File

@ -96,14 +96,26 @@ namespace osu.Game.Rulesets
context.SaveChanges(); context.SaveChanges();
// add any other modes
var existingRulesets = context.RulesetInfo.ToList(); var existingRulesets = context.RulesetInfo.ToList();
// add any other rulesets which have assemblies present but are not yet in the database.
foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
{ {
if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
{
var existingSameShortName = existingRulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
if (existingSameShortName != null)
{
// even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
// this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
// in such cases, update the instantiation info of the existing entry to point to the new one.
existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
}
else
context.RulesetInfo.Add(r.RulesetInfo); context.RulesetInfo.Add(r.RulesetInfo);
} }
}
context.SaveChanges(); context.SaveChanges();

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Screens; using osu.Framework.Screens;
@ -54,9 +55,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true); return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true);
} }
protected override void PrepareScoreForResults(Score score) protected override async Task PrepareScoreForResultsAsync(Score score)
{ {
base.PrepareScoreForResults(score); await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
} }

View File

@ -295,12 +295,12 @@ namespace osu.Game.Screens.Play
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded => DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
{ {
if (storyboardEnded.NewValue && resultsDisplayDelegate == null) if (storyboardEnded.NewValue)
updateCompletionState(); progressToResults(true);
}; };
// Bind the judgement processors to ourselves // Bind the judgement processors to ourselves
ScoreProcessor.HasCompleted.BindValueChanged(_ => updateCompletionState()); ScoreProcessor.HasCompleted.BindValueChanged(scoreCompletionChanged);
HealthProcessor.Failed += onFail; HealthProcessor.Failed += onFail;
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>()) foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
@ -374,7 +374,7 @@ namespace osu.Game.Screens.Play
}, },
skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0) skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0)
{ {
RequestSkip = () => updateCompletionState(true), RequestSkip = () => progressToResults(false),
Alpha = 0 Alpha = 0
}, },
FailOverlay = new FailOverlay FailOverlay = new FailOverlay
@ -626,7 +626,7 @@ namespace osu.Game.Screens.Play
/// <summary> /// <summary>
/// This delegate, when set, means the results screen has been queued to appear. /// This delegate, when set, means the results screen has been queued to appear.
/// The display of the results screen may be delayed by any work being done in <see cref="PrepareScoreForResults"/> and <see cref="PrepareScoreForResultsAsync"/>. /// The display of the results screen may be delayed by any work being done in <see cref="PrepareScoreForResultsAsync"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Once set, this can *only* be cancelled by rewinding, ie. if <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="false"/>. /// Once set, this can *only* be cancelled by rewinding, ie. if <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="false"/>.
@ -643,9 +643,8 @@ namespace osu.Game.Screens.Play
/// <summary> /// <summary>
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime. /// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
/// </summary> /// </summary>
/// <param name="skipStoryboardOutro">If in a state where a storyboard outro is to be played, offers the choice of skipping beyond it.</param>
/// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception> /// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception>
private void updateCompletionState(bool skipStoryboardOutro = false) private void scoreCompletionChanged(ValueChangedEvent<bool> completed)
{ {
// If this player instance is in the middle of an exit, don't attempt any kind of state update. // If this player instance is in the middle of an exit, don't attempt any kind of state update.
if (!this.IsCurrentScreen()) if (!this.IsCurrentScreen())
@ -656,7 +655,7 @@ namespace osu.Game.Screens.Play
// Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run). // Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run).
// In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done, // In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done,
// but it still doesn't feel right that this exists here. // but it still doesn't feel right that this exists here.
if (!ScoreProcessor.HasCompleted.Value) if (!completed.NewValue)
{ {
resultsDisplayDelegate?.Cancel(); resultsDisplayDelegate?.Cancel();
resultsDisplayDelegate = null; resultsDisplayDelegate = null;
@ -666,9 +665,6 @@ namespace osu.Game.Screens.Play
return; return;
} }
if (resultsDisplayDelegate != null)
throw new InvalidOperationException(@$"{nameof(updateCompletionState)} should never be fired more than once.");
// Only show the completion screen if the player hasn't failed // Only show the completion screen if the player hasn't failed
if (HealthProcessor.HasFailed) if (HealthProcessor.HasFailed)
return; return;
@ -683,32 +679,27 @@ namespace osu.Game.Screens.Play
if (!Configuration.ShowResults) if (!Configuration.ShowResults)
return; return;
// Asynchronously run score preparation operations (database import, online submission etc.).
prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults); prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults);
if (skipStoryboardOutro)
{
scheduleCompletion();
return;
}
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value; bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
if (storyboardHasOutro) if (storyboardHasOutro)
{ {
// if the current beatmap has a storyboard, the progression to results will be handled by the storyboard ending
// or the user pressing the skip outro button.
skipOutroOverlay.Show(); skipOutroOverlay.Show();
return; return;
} }
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) progressToResults(true);
scheduleCompletion();
} }
/// <summary>
/// Asynchronously run score preparation operations (database import, online submission etc.).
/// </summary>
/// <returns>The final score.</returns>
private async Task<ScoreInfo> prepareScoreForResults() private async Task<ScoreInfo> prepareScoreForResults()
{ {
// ReSharper disable once MethodHasAsyncOverload
PrepareScoreForResults(Score);
try try
{ {
await PrepareScoreForResultsAsync(Score).ConfigureAwait(false); await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
@ -730,18 +721,44 @@ namespace osu.Game.Screens.Play
return Score.ScoreInfo; return Score.ScoreInfo;
} }
private void scheduleCompletion() => resultsDisplayDelegate = Schedule(() => /// <summary>
/// Queue the results screen for display.
/// </summary>
/// <remarks>
/// A final display will only occur once all work is completed in <see cref="PrepareScoreForResultsAsync"/>. This means that even after calling this method, the results screen will never be shown until <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="true"/>.
///
/// Calling this method multiple times will have no effect.
/// </remarks>
/// <param name="withDelay">Whether a minimum delay (<see cref="RESULTS_DISPLAY_DELAY"/>) should be added before the screen is displayed.</param>
private void progressToResults(bool withDelay)
{ {
if (!prepareScoreForDisplayTask.IsCompleted) if (resultsDisplayDelegate != null)
{ // Note that if progressToResults is called one withDelay=true and then withDelay=false, this no-delay timing will not be
scheduleCompletion(); // accounted for. shouldn't be a huge concern (a user pressing the skip button after a results progression has already been queued
// may take x00 more milliseconds than expected in the very rare edge case).
//
// If required we can handle this more correctly by rescheduling here.
return;
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
resultsDisplayDelegate = new ScheduledDelegate(() =>
{
if (prepareScoreForDisplayTask?.IsCompleted != true)
// If the asynchronous preparation has not completed, keep repeating this delegate.
return;
resultsDisplayDelegate?.Cancel();
if (!this.IsCurrentScreen())
// This player instance may already be in the process of exiting.
return; return;
}
// screen may be in the exiting transition phase.
if (this.IsCurrentScreen())
this.Push(CreateResults(prepareScoreForDisplayTask.Result)); this.Push(CreateResults(prepareScoreForDisplayTask.Result));
}); }, Time.Current + delay, 50);
Scheduler.Add(resultsDisplayDelegate);
}
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
@ -939,14 +956,6 @@ namespace osu.Game.Screens.Play
{ {
screenSuspension?.Expire(); screenSuspension?.Expire();
// if the results screen is prepared to be displayed, forcefully show it on an exit request.
// usually if a user has completed a play session they do want to see results. and if they don't they can hit the same key a second time.
if (resultsDisplayDelegate != null && !resultsDisplayDelegate.Cancelled && !resultsDisplayDelegate.Completed)
{
resultsDisplayDelegate.RunTask();
return true;
}
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous. // EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
// To resolve test failures, forcefully end playing synchronously when this screen exits. // To resolve test failures, forcefully end playing synchronously when this screen exits.
// Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method. // Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method.
@ -1007,22 +1016,15 @@ namespace osu.Game.Screens.Play
/// <summary> /// <summary>
/// Prepare the <see cref="Scoring.Score"/> for display at results. /// Prepare the <see cref="Scoring.Score"/> for display at results.
/// </summary> /// </summary>
/// <remarks>
/// This is run synchronously before <see cref="PrepareScoreForResultsAsync"/> is run.
/// </remarks>
/// <param name="score">The <see cref="Scoring.Score"/> to prepare.</param> /// <param name="score">The <see cref="Scoring.Score"/> to prepare.</param>
protected virtual void PrepareScoreForResults(Score score) /// <returns>A task that prepares the provided score. On completion, the score is assumed to be ready for display.</returns>
protected virtual Task PrepareScoreForResultsAsync(Score score)
{ {
// perform one final population to ensure everything is up-to-date. // perform one final population to ensure everything is up-to-date.
ScoreProcessor.PopulateScore(score.ScoreInfo); ScoreProcessor.PopulateScore(score.ScoreInfo);
}
/// <summary> return Task.CompletedTask;
/// Prepare the <see cref="Scoring.Score"/> for display at results. }
/// </summary>
/// <param name="score">The <see cref="Scoring.Score"/> to prepare.</param>
/// <returns>A task that prepares the provided score. On completion, the score is assumed to be ready for display.</returns>
protected virtual Task PrepareScoreForResultsAsync(Score score) => Task.CompletedTask;
/// <summary> /// <summary>
/// Creates the <see cref="ResultsScreen"/> for a <see cref="ScoreInfo"/>. /// Creates the <see cref="ResultsScreen"/> for a <see cref="ScoreInfo"/>.

View File

@ -25,7 +25,7 @@ namespace osu.Game.Storyboards.Drawables
/// </summary> /// </summary>
public IBindable<bool> HasStoryboardEnded => hasStoryboardEnded; public IBindable<bool> HasStoryboardEnded => hasStoryboardEnded;
private readonly BindableBool hasStoryboardEnded = new BindableBool(); private readonly BindableBool hasStoryboardEnded = new BindableBool(true);
protected override Container<DrawableStoryboardLayer> Content { get; } protected override Container<DrawableStoryboardLayer> Content { get; }

View File

@ -60,6 +60,9 @@ namespace osu.Game.Utils
{ {
foreach (var invalid in combination.Where(m => type.IsInstanceOfType(m))) foreach (var invalid in combination.Where(m => type.IsInstanceOfType(m)))
{ {
if (invalid == mod)
continue;
invalidMods ??= new List<Mod>(); invalidMods ??= new List<Mod>();
invalidMods.Add(invalid); invalidMods.Add(invalid);
} }