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

Merge branch 'master' into bassmix

This commit is contained in:
Dean Herbert 2021-08-05 17:33:21 +09:00
commit 1476b3b22a
221 changed files with 3188 additions and 1687 deletions

View File

@ -190,3 +190,5 @@ dotnet_diagnostic.CA2225.severity = none
# Banned APIs # Banned APIs
dotnet_diagnostic.RS0030.severity = error dotnet_diagnostic.RS0030.severity = error
dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text.

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.722.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.803.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.723.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.804.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -64,7 +64,7 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Xamarin.Essentials" Version="1.6.1" /> <PackageReference Include="Xamarin.Essentials" Version="1.7.0" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project> </Project>

View File

@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var skin = new TestSkin { FlipCatcherPlate = flip }; var skin = new TestSkin { FlipCatcherPlate = flip };
container.Child = new SkinProvidingContainer(skin) container.Child = new SkinProvidingContainer(skin)
{ {
Child = catcher = new Catcher(new Container(), new DroppedObjectContainer()) Child = catcher = new Catcher(new DroppedObjectContainer())
{ {
Anchor = Anchor.Centre Anchor = Anchor.Centre
} }

View File

@ -31,8 +31,6 @@ namespace osu.Game.Rulesets.Catch.Tests
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
private Container trailContainer;
private DroppedObjectContainer droppedObjectContainer; private DroppedObjectContainer droppedObjectContainer;
private TestCatcher catcher; private TestCatcher catcher;
@ -45,7 +43,6 @@ namespace osu.Game.Rulesets.Catch.Tests
CircleSize = 0, CircleSize = 0,
}; };
trailContainer = new Container();
droppedObjectContainer = new DroppedObjectContainer(); droppedObjectContainer = new DroppedObjectContainer();
Child = new Container Child = new Container
@ -54,8 +51,7 @@ namespace osu.Game.Rulesets.Catch.Tests
Children = new Drawable[] Children = new Drawable[]
{ {
droppedObjectContainer, droppedObjectContainer,
catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty), catcher = new TestCatcher(droppedObjectContainer, difficulty),
trailContainer,
} }
}; };
}); });
@ -294,8 +290,8 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>(); public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
public TestCatcher(Container trailsTarget, DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty) public TestCatcher(DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty)
: base(trailsTarget, droppedObjectTarget, difficulty) : base(droppedObjectTarget, difficulty)
{ {
} }
} }

View File

@ -122,10 +122,9 @@ namespace osu.Game.Rulesets.Catch.Tests
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty) public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
{ {
var droppedObjectContainer = new DroppedObjectContainer(); var droppedObjectContainer = new DroppedObjectContainer();
Add(droppedObjectContainer); Add(droppedObjectContainer);
Catcher = new Catcher(this, droppedObjectContainer, beatmapDifficulty) Catcher = new Catcher(droppedObjectContainer, beatmapDifficulty)
{ {
X = CatchPlayfield.CENTER_X X = CatchPlayfield.CENTER_X
}; };

View File

@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Catch.Tests
} }
[Test] [Test]
public void TestCustomEndGlowColour() public void TestCustomAfterImageColour()
{ {
var skin = new TestSkin var skin = new TestSkin
{ {
@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Tests
} }
[Test] [Test]
public void TestCustomEndGlowColourPriority() public void TestCustomAfterImageColourPriority()
{ {
var skin = new TestSkin var skin = new TestSkin
{ {
@ -111,39 +111,37 @@ namespace osu.Game.Rulesets.Catch.Tests
checkHyperDashFruitColour(skin, skin.HyperDashColour); checkHyperDashFruitColour(skin, skin.HyperDashColour);
} }
private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedEndGlowColour = null) private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedAfterImageColour = null)
{ {
Container trailsContainer = null;
Catcher catcher = null;
CatcherTrailDisplay trails = null; CatcherTrailDisplay trails = null;
Catcher catcher = null;
AddStep("create hyper-dashing catcher", () => AddStep("create hyper-dashing catcher", () =>
{ {
trailsContainer = new Container(); CatcherArea catcherArea;
Child = setupSkinHierarchy(new Container Child = setupSkinHierarchy(new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Children = new Drawable[] Child = catcherArea = new CatcherArea
{ {
catcher = new Catcher(trailsContainer, new DroppedObjectContainer()) Catcher = catcher = new Catcher(new DroppedObjectContainer())
{ {
Scale = new Vector2(4) Scale = new Vector2(4)
}, }
trailsContainer
} }
}, skin); }, skin);
trails = catcherArea.ChildrenOfType<CatcherTrailDisplay>().Single();
}); });
AddStep("get trails container", () => AddStep("start hyper-dash", () =>
{ {
trails = trailsContainer.OfType<CatcherTrailDisplay>().Single();
catcher.SetHyperDashState(2); catcher.SetHyperDashState(2);
}); });
AddUntilStep("catcher colour is correct", () => catcher.Colour == expectedCatcherColour); AddUntilStep("catcher colour is correct", () => catcher.Colour == expectedCatcherColour);
AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour); AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour);
AddAssert("catcher end-glow colours are correct", () => trails.EndGlowSpritesColour == (expectedEndGlowColour ?? expectedCatcherColour)); AddAssert("catcher after-image colours are correct", () => trails.HyperDashAfterImageColour == (expectedAfterImageColour ?? expectedCatcherColour));
AddStep("finish hyper-dashing", () => AddStep("finish hyper-dashing", () =>
{ {

View File

@ -130,7 +130,8 @@ namespace osu.Game.Rulesets.Catch
return new Mod[] return new Mod[]
{ {
new MultiMod(new ModWindUp(), new ModWindDown()), new MultiMod(new ModWindUp(), new ModWindDown()),
new CatchModFloatingFruits() new CatchModFloatingFruits(),
new CatchModMuted(),
}; };
default: default:

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.
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModMuted : ModMuted<CatchHitObject>
{
}
}

View File

@ -3,7 +3,6 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
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;
@ -45,14 +44,9 @@ namespace osu.Game.Rulesets.Catch.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
var trailContainer = new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft
};
var droppedObjectContainer = new DroppedObjectContainer(); var droppedObjectContainer = new DroppedObjectContainer();
Catcher = new Catcher(trailContainer, droppedObjectContainer, difficulty) Catcher = new Catcher(droppedObjectContainer, difficulty)
{ {
X = CENTER_X X = CENTER_X
}; };
@ -70,7 +64,6 @@ namespace osu.Game.Rulesets.Catch.UI
Origin = Anchor.TopLeft, Origin = Anchor.TopLeft,
Catcher = Catcher, Catcher = Catcher,
}, },
trailContainer,
HitObjectContainer, HitObjectContainer,
}); });

View File

@ -36,8 +36,7 @@ namespace osu.Game.Rulesets.Catch.UI
public const float ALLOWED_CATCH_RANGE = 0.8f; public const float ALLOWED_CATCH_RANGE = 0.8f;
/// <summary> /// <summary>
/// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail /// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail and after-image during a hyper-dash.
/// and end glow/after-image during a hyper-dash.
/// </summary> /// </summary>
public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red; public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red;
@ -71,11 +70,6 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
private const float caught_fruit_scale_adjust = 0.5f; private const float caught_fruit_scale_adjust = 0.5f;
[NotNull]
private readonly Container trailsTarget;
private CatcherTrailDisplay trails;
/// <summary> /// <summary>
/// Contains caught objects on the plate. /// Contains caught objects on the plate.
/// </summary> /// </summary>
@ -88,30 +82,22 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherAnimationState CurrentState public CatcherAnimationState CurrentState
{ {
get => Body.AnimationState.Value; get => body.AnimationState.Value;
private set => Body.AnimationState.Value = value; private set => body.AnimationState.Value = value;
} }
private bool dashing; /// <summary>
/// Whether the catcher is currently dashing.
public bool Dashing /// </summary>
{ public bool Dashing { get; set; }
get => dashing;
set
{
if (value == dashing) return;
dashing = value;
updateTrailVisibility();
}
}
/// <summary> /// <summary>
/// The currently facing direction. /// The currently facing direction.
/// </summary> /// </summary>
public Direction VisualDirection { get; set; } = Direction.Right; public Direction VisualDirection { get; set; } = Direction.Right;
public Vector2 BodyScale => Scale * body.Scale;
/// <summary> /// <summary>
/// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed. /// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
/// </summary> /// </summary>
@ -122,10 +108,9 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
private readonly float catchWidth; private readonly float catchWidth;
internal readonly SkinnableCatcher Body; 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 double hyperDashModifier = 1; private double hyperDashModifier = 1;
private int hyperDashDirection; private int hyperDashDirection;
@ -138,9 +123,8 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly DrawablePool<CaughtBanana> caughtBananaPool; private readonly DrawablePool<CaughtBanana> caughtBananaPool;
private readonly DrawablePool<CaughtDroplet> caughtDropletPool; private readonly DrawablePool<CaughtDroplet> caughtDropletPool;
public Catcher([NotNull] Container trailsTarget, [NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null) public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null)
{ {
this.trailsTarget = trailsTarget;
this.droppedObjectTarget = droppedObjectTarget; this.droppedObjectTarget = droppedObjectTarget;
Origin = Anchor.TopCentre; Origin = Anchor.TopCentre;
@ -164,7 +148,7 @@ namespace osu.Game.Rulesets.Catch.UI
// offset fruit vertically to better place "above" the plate. // offset fruit vertically to better place "above" the plate.
Y = -5 Y = -5
}, },
Body = new SkinnableCatcher(), body = new SkinnableCatcher(),
hitExplosionContainer = new HitExplosionContainer hitExplosionContainer = new HitExplosionContainer
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
@ -177,15 +161,6 @@ namespace osu.Game.Rulesets.Catch.UI
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
hitLighting = config.GetBindable<bool>(OsuSetting.HitLighting); hitLighting = config.GetBindable<bool>(OsuSetting.HitLighting);
trails = new CatcherTrailDisplay(this);
}
protected override void LoadComplete()
{
base.LoadComplete();
// don't add in above load as we may potentially modify a parent in an unsafe manner.
trailsTarget.Add(trails);
} }
/// <summary> /// <summary>
@ -307,12 +282,9 @@ namespace osu.Game.Rulesets.Catch.UI
hyperDashTargetPosition = targetPosition; hyperDashTargetPosition = targetPosition;
if (!wasHyperDashing) if (!wasHyperDashing)
{
trails.DisplayEndGlow();
runHyperDashStateTransition(true); runHyperDashStateTransition(true);
} }
} }
}
/// <summary> /// <summary>
/// Drop any fruit off the plate. /// Drop any fruit off the plate.
@ -326,13 +298,9 @@ namespace osu.Game.Rulesets.Catch.UI
private void runHyperDashStateTransition(bool hyperDashing) private void runHyperDashStateTransition(bool hyperDashing)
{ {
updateTrailVisibility();
this.FadeColour(hyperDashing ? hyperDashColour : Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); this.FadeColour(hyperDashing ? hyperDashColour : Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
} }
private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
protected override void SkinChanged(ISkinSource skin) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin); base.SkinChanged(skin);
@ -341,13 +309,6 @@ namespace osu.Game.Rulesets.Catch.UI
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ?? skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??
DEFAULT_HYPER_DASH_COLOUR; DEFAULT_HYPER_DASH_COLOUR;
hyperDashEndGlowColour =
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value ??
hyperDashColour;
trails.HyperDashTrailsColour = hyperDashColour;
trails.EndGlowSpritesColour = hyperDashEndGlowColour;
flipCatcherPlate = skin.GetConfig<CatchSkinConfiguration, bool>(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true; flipCatcherPlate = skin.GetConfig<CatchSkinConfiguration, bool>(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true;
runHyperDashStateTransition(HyperDashing); runHyperDashStateTransition(HyperDashing);
@ -358,7 +319,7 @@ namespace osu.Game.Rulesets.Catch.UI
base.Update(); base.Update();
var scaleFromDirection = new Vector2((int)VisualDirection, 1); var scaleFromDirection = new Vector2((int)VisualDirection, 1);
Body.Scale = scaleFromDirection; body.Scale = scaleFromDirection;
caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One; caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
// Correct overshooting. // Correct overshooting.

View File

@ -25,17 +25,15 @@ namespace osu.Game.Rulesets.Catch.UI
public Catcher Catcher public Catcher Catcher
{ {
get => catcher; get => catcher;
set set => catcherContainer.Child = catcher = value;
{ }
if (catcher != null)
Remove(catcher);
Add(catcher = value); private readonly Container<Catcher> catcherContainer;
}
}
private readonly CatchComboDisplay comboDisplay; private readonly CatchComboDisplay comboDisplay;
private readonly CatcherTrailDisplay catcherTrails;
private Catcher catcher; private Catcher catcher;
/// <summary> /// <summary>
@ -45,13 +43,20 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
private int currentDirection; private int currentDirection;
// TODO: support replay rewind
private bool lastHyperDashState;
/// <remarks> /// <remarks>
/// <see cref="Catcher"/> must be set before loading. /// <see cref="Catcher"/> must be set before loading.
/// </remarks> /// </remarks>
public CatcherArea() public CatcherArea()
{ {
Size = new Vector2(CatchPlayfield.WIDTH, Catcher.BASE_SIZE); Size = new Vector2(CatchPlayfield.WIDTH, Catcher.BASE_SIZE);
Child = comboDisplay = new CatchComboDisplay Children = new Drawable[]
{
catcherContainer = new Container<Catcher> { RelativeSizeAxes = Axes.Both },
catcherTrails = new CatcherTrailDisplay(),
comboDisplay = new CatchComboDisplay
{ {
RelativeSizeAxes = Axes.None, RelativeSizeAxes = Axes.None,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
@ -59,6 +64,7 @@ namespace osu.Game.Rulesets.Catch.UI
Origin = Anchor.Centre, Origin = Anchor.Centre,
Margin = new MarginPadding { Bottom = 350f }, Margin = new MarginPadding { Bottom = 350f },
X = CatchPlayfield.CENTER_X X = CatchPlayfield.CENTER_X
}
}; };
} }
@ -102,6 +108,27 @@ namespace osu.Game.Rulesets.Catch.UI
base.UpdateAfterChildren(); base.UpdateAfterChildren();
comboDisplay.X = Catcher.X; comboDisplay.X = Catcher.X;
if (Time.Elapsed <= 0)
{
// This is probably a wrong value, but currently the true value is not recorded.
// Setting `true` will prevent generation of false-positive after-images (with more false-negatives).
lastHyperDashState = true;
return;
}
if (!lastHyperDashState && Catcher.HyperDashing)
displayCatcherTrail(CatcherTrailAnimation.HyperDashAfterImage);
if (Catcher.Dashing || Catcher.HyperDashing)
{
double generationInterval = Catcher.HyperDashing ? 25 : 50;
if (Time.Current - catcherTrails.LastDashTrailTime >= generationInterval)
displayCatcherTrail(Catcher.HyperDashing ? CatcherTrailAnimation.HyperDashing : CatcherTrailAnimation.Dashing);
}
lastHyperDashState = Catcher.HyperDashing;
} }
public void SetCatcherPosition(float X) public void SetCatcherPosition(float X)
@ -154,5 +181,7 @@ namespace osu.Game.Rulesets.Catch.UI
break; break;
} }
} }
private void displayCatcherTrail(CatcherTrailAnimation animation) => catcherTrails.Add(new CatcherTrailEntry(Time.Current, Catcher.CurrentState, Catcher.X, Catcher.BodyScale, animation));
} }
} }

View File

@ -2,8 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Rulesets.Objects.Pooling;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.UI namespace osu.Game.Rulesets.Catch.UI
@ -12,13 +12,8 @@ namespace osu.Game.Rulesets.Catch.UI
/// A trail of the catcher. /// A trail of the catcher.
/// It also represents a hyper dash afterimage. /// It also represents a hyper dash afterimage.
/// </summary> /// </summary>
public class CatcherTrail : PoolableDrawable public class CatcherTrail : PoolableDrawableWithLifetime<CatcherTrailEntry>
{ {
public CatcherAnimationState AnimationState
{
set => body.AnimationState.Value = value;
}
private readonly SkinnableCatcher body; private readonly SkinnableCatcher body;
public CatcherTrail() public CatcherTrail()
@ -34,11 +29,40 @@ namespace osu.Game.Rulesets.Catch.UI
}; };
} }
protected override void FreeAfterUse() protected override void OnApply(CatcherTrailEntry entry)
{ {
Position = new Vector2(entry.Position, 0);
Scale = entry.Scale;
body.AnimationState.Value = entry.CatcherState;
using (BeginAbsoluteSequence(entry.LifetimeStart, false))
applyTransforms(entry.Animation);
}
protected override void OnFree(CatcherTrailEntry entry)
{
ApplyTransformsAt(double.MinValue);
ClearTransforms(); ClearTransforms();
Alpha = 1; }
base.FreeAfterUse();
private void applyTransforms(CatcherTrailAnimation animation)
{
switch (animation)
{
case CatcherTrailAnimation.Dashing:
case CatcherTrailAnimation.HyperDashing:
this.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
break;
case CatcherTrailAnimation.HyperDashAfterImage:
this.MoveToOffset(new Vector2(0, -10), 1200, Easing.In);
this.ScaleTo(Scale * 0.95f).ScaleTo(Scale * 1.2f, 1200, Easing.In);
this.FadeOut(1200);
break;
}
Expire();
} }
} }
} }

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.Catch.UI
{
public enum CatcherTrailAnimation
{
Dashing,
HyperDashing,
HyperDashAfterImage
}
}

View File

@ -2,11 +2,13 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using JetBrains.Annotations; using osu.Framework.Allocation;
using osu.Framework.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 osuTK; using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Rulesets.Objects.Pooling;
using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI namespace osu.Game.Rulesets.Catch.UI
@ -15,70 +17,32 @@ namespace osu.Game.Rulesets.Catch.UI
/// Represents a component responsible for displaying /// Represents a component responsible for displaying
/// the appropriate catcher trails when requested to. /// the appropriate catcher trails when requested to.
/// </summary> /// </summary>
public class CatcherTrailDisplay : CompositeDrawable public class CatcherTrailDisplay : PooledDrawableWithLifetimeContainer<CatcherTrailEntry, CatcherTrail>
{ {
private readonly Catcher catcher; /// <summary>
/// The most recent time a dash trail was added to this container.
/// Only alive (not faded out) trails are considered.
/// Returns <see cref="double.NegativeInfinity"/> if no dash trail is alive.
/// </summary>
public double LastDashTrailTime => getLastDashTrailTime();
public Color4 HyperDashTrailsColour => hyperDashTrails.Colour;
public Color4 HyperDashAfterImageColour => hyperDashAfterImages.Colour;
protected override bool RemoveRewoundEntry => true;
private readonly DrawablePool<CatcherTrail> trailPool; private readonly DrawablePool<CatcherTrail> trailPool;
private readonly Container<CatcherTrail> dashTrails; private readonly Container<CatcherTrail> dashTrails;
private readonly Container<CatcherTrail> hyperDashTrails; private readonly Container<CatcherTrail> hyperDashTrails;
private readonly Container<CatcherTrail> endGlowSprites; private readonly Container<CatcherTrail> hyperDashAfterImages;
private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR; [Resolved]
private ISkinSource skin { get; set; }
public Color4 HyperDashTrailsColour public CatcherTrailDisplay()
{ {
get => hyperDashTrailsColour;
set
{
if (hyperDashTrailsColour == value)
return;
hyperDashTrailsColour = value;
hyperDashTrails.Colour = hyperDashTrailsColour;
}
}
private Color4 endGlowSpritesColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
public Color4 EndGlowSpritesColour
{
get => endGlowSpritesColour;
set
{
if (endGlowSpritesColour == value)
return;
endGlowSpritesColour = value;
endGlowSprites.Colour = endGlowSpritesColour;
}
}
private bool trail;
/// <summary>
/// Whether to start displaying trails following the catcher.
/// </summary>
public bool DisplayTrail
{
get => trail;
set
{
if (trail == value)
return;
trail = value;
if (trail)
displayTrail();
}
}
public CatcherTrailDisplay([NotNull] Catcher catcher)
{
this.catcher = catcher ?? throw new ArgumentNullException(nameof(catcher));
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
@ -86,47 +50,86 @@ namespace osu.Game.Rulesets.Catch.UI
trailPool = new DrawablePool<CatcherTrail>(30), trailPool = new DrawablePool<CatcherTrail>(30),
dashTrails = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both }, dashTrails = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both },
hyperDashTrails = new Container<CatcherTrail> { 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<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR }, hyperDashAfterImages = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
}; };
} }
/// <summary> protected override void LoadComplete()
/// Displays a single end-glow catcher sprite.
/// </summary>
public void DisplayEndGlow()
{ {
var endGlow = createTrailSprite(endGlowSprites); base.LoadComplete();
endGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In); skin.SourceChanged += skinSourceChanged;
endGlow.ScaleTo(endGlow.Scale * 0.95f).ScaleTo(endGlow.Scale * 1.2f, 1200, Easing.In); skinSourceChanged();
endGlow.FadeOut(1200);
endGlow.Expire(true);
} }
private void displayTrail() private void skinSourceChanged()
{ {
if (!DisplayTrail) hyperDashTrails.Colour = skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ?? Catcher.DEFAULT_HYPER_DASH_COLOUR;
return; hyperDashAfterImages.Colour = skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value ?? hyperDashTrails.Colour;
var sprite = createTrailSprite(catcher.HyperDashing ? hyperDashTrails : dashTrails);
sprite.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
sprite.Expire(true);
Scheduler.AddDelayed(displayTrail, catcher.HyperDashing ? 25 : 50);
} }
private CatcherTrail createTrailSprite(Container<CatcherTrail> target) protected override void AddDrawable(CatcherTrailEntry entry, CatcherTrail drawable)
{ {
CatcherTrail sprite = trailPool.Get(); switch (entry.Animation)
{
case CatcherTrailAnimation.Dashing:
dashTrails.Add(drawable);
break;
sprite.AnimationState = catcher.CurrentState; case CatcherTrailAnimation.HyperDashing:
sprite.Scale = catcher.Scale * catcher.Body.Scale; hyperDashTrails.Add(drawable);
sprite.Position = catcher.Position; break;
target.Add(sprite); case CatcherTrailAnimation.HyperDashAfterImage:
hyperDashAfterImages.Add(drawable);
break;
}
}
return sprite; protected override void RemoveDrawable(CatcherTrailEntry entry, CatcherTrail drawable)
{
switch (entry.Animation)
{
case CatcherTrailAnimation.Dashing:
dashTrails.Remove(drawable);
break;
case CatcherTrailAnimation.HyperDashing:
hyperDashTrails.Remove(drawable);
break;
case CatcherTrailAnimation.HyperDashAfterImage:
hyperDashAfterImages.Remove(drawable);
break;
}
}
protected override CatcherTrail GetDrawable(CatcherTrailEntry entry)
{
CatcherTrail trail = trailPool.Get();
trail.Apply(entry);
return trail;
}
private double getLastDashTrailTime()
{
double maxTime = double.NegativeInfinity;
foreach (var trail in dashTrails)
maxTime = Math.Max(maxTime, trail.LifetimeStart);
foreach (var trail in hyperDashTrails)
maxTime = Math.Max(maxTime, trail.LifetimeStart);
return maxTime;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (skin != null)
skin.SourceChanged -= skinSourceChanged;
} }
} }
} }

View File

@ -0,0 +1,31 @@
// 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;
namespace osu.Game.Rulesets.Catch.UI
{
public class CatcherTrailEntry : LifetimeEntry
{
public readonly CatcherAnimationState CatcherState;
public readonly float Position;
/// <summary>
/// The scaling of the catcher body. It also represents a flipped catcher (negative x component).
/// </summary>
public readonly Vector2 Scale;
public readonly CatcherTrailAnimation Animation;
public CatcherTrailEntry(double startTime, CatcherAnimationState catcherState, float position, Vector2 scale, CatcherTrailAnimation animation)
{
LifetimeStart = startTime;
CatcherState = catcherState;
Position = position;
Scale = scale;
Animation = animation;
}
}
}

View File

@ -253,7 +253,8 @@ namespace osu.Game.Rulesets.Mania
case ModType.Fun: case ModType.Fun:
return new Mod[] return new Mod[]
{ {
new MultiMod(new ModWindUp(), new ModWindDown()) new MultiMod(new ModWindUp(), new ModWindDown()),
new ManiaModMuted(),
}; };
default: default:

View File

@ -10,13 +10,9 @@ using osu.Game.Rulesets.Mania.Beatmaps;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModMirror : Mod, IApplicableToBeatmap public class ManiaModMirror : ModMirror, IApplicableToBeatmap
{ {
public override string Name => "Mirror";
public override string Acronym => "MR";
public override ModType Type => ModType.Conversion;
public override string Description => "Notes are flipped horizontally."; public override string Description => "Notes are flipped horizontally.";
public override double ScoreMultiplier => 1;
public void ApplyToBeatmap(IBeatmap beatmap) public void ApplyToBeatmap(IBeatmap beatmap)
{ {

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.
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModMuted : ModMuted<ManiaHitObject>
{
}
}

View File

@ -0,0 +1,52 @@
// 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.Testing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModMuted : OsuModTestScene
{
/// <summary>
/// Ensures that a final volume combo of 0 (i.e. "always muted" mode) constantly plays metronome and completely mutes track.
/// </summary>
[Test]
public void TestZeroFinalCombo() => CreateModTest(new ModTestData
{
Mod = new OsuModMuted
{
MuteComboCount = { Value = 0 },
},
PassCondition = () => Beatmap.Value.Track.AggregateVolume.Value == 0.0 &&
Player.ChildrenOfType<Metronome>().SingleOrDefault()?.AggregateVolume.Value == 1.0,
});
/// <summary>
/// Ensures that copying from a normal mod with 0 final combo while originally inversed does not yield incorrect results.
/// </summary>
[Test]
public void TestModCopy()
{
OsuModMuted muted = null;
AddStep("create inversed mod", () => muted = new OsuModMuted
{
MuteComboCount = { Value = 100 },
InverseMuting = { Value = true },
});
AddStep("copy from normal", () => muted.CopyFrom(new OsuModMuted
{
MuteComboCount = { Value = 0 },
InverseMuting = { Value = false },
}));
AddAssert("mute combo count = 0", () => muted.MuteComboCount.Value == 0);
AddAssert("inverse muting = false", () => muted.InverseMuting.Value == false);
}
}
}

View File

@ -1,13 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
@ -15,23 +14,13 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => 1.06;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModMirror)).ToArray();
public void ApplyToHitObject(HitObject hitObject) public void ApplyToHitObject(HitObject hitObject)
{ {
var osuObject = (OsuHitObject)hitObject; var osuObject = (OsuHitObject)hitObject;
osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y); OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
if (!(hitObject is Slider slider))
return;
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
foreach (var point in controlPoints)
point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y);
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
} }
} }
} }

View File

@ -0,0 +1,50 @@
// 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;
using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModMirror : ModMirror, IApplicableToHitObject
{
public override string Description => "Flip objects on the chosen axes.";
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) };
[SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")]
public Bindable<MirrorType> Reflection { get; } = new Bindable<MirrorType>();
public void ApplyToHitObject(HitObject hitObject)
{
var osuObject = (OsuHitObject)hitObject;
switch (Reflection.Value)
{
case MirrorType.Horizontal:
OsuHitObjectGenerationUtils.ReflectHorizontally(osuObject);
break;
case MirrorType.Vertical:
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
break;
case MirrorType.Both:
OsuHitObjectGenerationUtils.ReflectHorizontally(osuObject);
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
break;
}
}
public enum MirrorType
{
Horizontal,
Vertical,
Both
}
}
}

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.
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModMuted : ModMuted<OsuHitObject>
{
}
}

View File

@ -4,8 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
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;
@ -16,7 +14,6 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -29,7 +26,6 @@ using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.Utils; using osu.Game.Rulesets.Osu.Utils;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -67,11 +63,6 @@ namespace osu.Game.Rulesets.Osu.Mods
/// </summary> /// </summary>
private const float distance_cap = 380f; private const float distance_cap = 380f;
// The distances from the hit objects to the borders of the playfield they start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn
private const byte border_distance_x = 192;
private const byte border_distance_y = 144;
/// <summary> /// <summary>
/// The extent of rotation towards playfield centre when a circle is near the edge /// The extent of rotation towards playfield centre when a circle is near the edge
/// </summary> /// </summary>
@ -341,46 +332,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{ {
drawableRuleset.Overlays.Add(new TargetBeatContainer(drawableRuleset.Beatmap.HitObjects.First().StartTime)); drawableRuleset.Overlays.Add(new Metronome(drawableRuleset.Beatmap.HitObjects.First().StartTime));
}
public class TargetBeatContainer : BeatSyncedContainer
{
private readonly double firstHitTime;
private PausableSkinnableSound sample;
public TargetBeatContainer(double firstHitTime)
{
this.firstHitTime = firstHitTime;
AllowMistimedEventFiring = false;
Divisor = 1;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
sample = new PausableSkinnableSound(new SampleInfo("Gameplay/catch-banana"))
};
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
if (!IsBeatSyncedWithTrack) return;
int timeSignature = (int)timingPoint.TimeSignature;
// play metronome from one measure before the first object.
if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature)
return;
sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f;
sample.Play();
}
} }
#endregion #endregion

View File

@ -166,6 +166,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModDifficultyAdjust(), new OsuModDifficultyAdjust(),
new OsuModClassic(), new OsuModClassic(),
new OsuModRandom(), new OsuModRandom(),
new OsuModMirror(),
}; };
case ModType.Automation: case ModType.Automation:
@ -188,6 +189,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModTraceable(), new OsuModTraceable(),
new OsuModBarrelRoll(), new OsuModBarrelRoll(),
new OsuModApproachDifferent(), new OsuModApproachDifferent(),
new OsuModMuted(),
}; };
case ModType.System: case ModType.System:

View File

@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.UI
private void onJudgementLoaded(DrawableOsuJudgement judgement) private void onJudgementLoaded(DrawableOsuJudgement judgement)
{ {
judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent()); judgementAboveHitObjectLayer.Add(judgement.ProxiedAboveHitObjectsContent);
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
@ -150,6 +150,10 @@ namespace osu.Game.Rulesets.Osu.UI
DrawableOsuJudgement explosion = poolDictionary[result.Type].Get(doj => doj.Apply(result, judgedObject)); DrawableOsuJudgement explosion = poolDictionary[result.Type].Get(doj => doj.Apply(result, judgedObject));
judgementLayer.Add(explosion); judgementLayer.Add(explosion);
// the proxied content is added to judgementAboveHitObjectLayer once, on first load, and never removed from it.
// ensure that ordering is consistent with expectations (latest judgement should be front-most).
judgementAboveHitObjectLayer.ChangeChildDepth(explosion.ProxiedAboveHitObjectsContent, (float)-result.TimeAbsolute);
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);

View File

@ -2,7 +2,11 @@
// 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 osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Utils namespace osu.Game.Rulesets.Osu.Utils
@ -100,5 +104,47 @@ namespace osu.Game.Rulesets.Osu.Utils
initial.Length * MathF.Sin(finalAngleRad) initial.Length * MathF.Sin(finalAngleRad)
); );
} }
/// <summary>
/// Reflects the position of the <see cref="OsuHitObject"/> in the playfield horizontally.
/// </summary>
/// <param name="osuObject">The object to reflect.</param>
public static void ReflectHorizontally(OsuHitObject osuObject)
{
osuObject.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - osuObject.X, osuObject.Position.Y);
if (!(osuObject is Slider slider))
return;
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
foreach (var point in controlPoints)
point.Position.Value = new Vector2(-point.Position.Value.X, point.Position.Value.Y);
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
}
/// <summary>
/// Reflects the position of the <see cref="OsuHitObject"/> in the playfield vertically.
/// </summary>
/// <param name="osuObject">The object to reflect.</param>
public static void ReflectVertically(OsuHitObject osuObject)
{
osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y);
if (!(osuObject is Slider slider))
return;
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
foreach (var point in controlPoints)
point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y);
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
}
} }
} }

View File

@ -2,10 +2,30 @@
// 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.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.Mods namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModClassic : ModClassic public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>, IUpdatableByPlayfield
{ {
private DrawableTaikoRuleset drawableTaikoRuleset;
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
}
public void Update(Playfield playfield)
{
// Classic taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
const float scroll_rate = 10;
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
float ratio = drawableTaikoRuleset.DrawHeight / 480;
drawableTaikoRuleset.TimeRange.Value = (playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
}
} }
} }

View File

@ -12,23 +12,11 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Mods namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModHidden : ModHidden, IApplicableToDifficulty public class TaikoModHidden : ModHidden
{ {
public override string Description => @"Beats fade out before you hit them!"; public override string Description => @"Beats fade out before you hit them!";
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => 1.06;
/// <summary>
/// In osu-stable, the hit position is 160, so the active playfield is essentially 160 pixels shorter
/// than the actual screen width. The normalized playfield height is 480, so on a 4:3 screen the
/// playfield ratio of the active area up to the hit position will actually be (640 - 160) / 480 = 1.
/// For custom resolutions/aspect ratios (x:y), the screen width given the normalized height becomes 480 * x / y instead,
/// and the playfield ratio becomes (480 * x / y - 160) / 480 = x / y - 1/3.
/// This constant is equal to the playfield ratio on 4:3 screens divided by the playfield ratio on 16:9 screens.
/// </summary>
private const double hd_sv_scale = (4.0 / 3.0 - 1.0 / 3.0) / (16.0 / 9.0 - 1.0 / 3.0);
private double originalSliderMultiplier;
private ControlPointInfo controlPointInfo; private ControlPointInfo controlPointInfo;
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
@ -41,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
double beatLength = controlPointInfo.TimingPointAt(position).BeatLength; double beatLength = controlPointInfo.TimingPointAt(position).BeatLength;
double speedMultiplier = controlPointInfo.DifficultyPointAt(position).SpeedMultiplier; double speedMultiplier = controlPointInfo.DifficultyPointAt(position).SpeedMultiplier;
return originalSliderMultiplier * speedMultiplier * TimingControlPoint.DEFAULT_BEAT_LENGTH / beatLength; return speedMultiplier * TimingControlPoint.DEFAULT_BEAT_LENGTH / beatLength;
} }
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
@ -69,22 +57,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
} }
} }
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
{
}
public void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
// needs to be read after all processing has been run (TaikoBeatmapConverter applies an adjustment which would otherwise be omitted).
originalSliderMultiplier = difficulty.SliderMultiplier;
// osu-stable has an added playfield cover that essentially forces a 4:3 playfield ratio, by cutting off all objects past that size.
// This is not yet implemented; instead a playfield adjustment container is present which maintains a 16:9 ratio.
// For now, increase the slider multiplier proportionally so that the notes stay on the screen for the same amount of time as on stable.
// Note that this means that the notes will scroll faster as they have a longer distance to travel on the screen in that same amount of time.
difficulty.SliderMultiplier /= hd_sv_scale;
}
public override void ApplyToBeatmap(IBeatmap beatmap) public override void ApplyToBeatmap(IBeatmap beatmap)
{ {
controlPointInfo = beatmap.ControlPointInfo; controlPointInfo = beatmap.ControlPointInfo;

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.
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModMuted : ModMuted<TaikoHitObject>
{
}
}

View File

@ -149,7 +149,8 @@ namespace osu.Game.Rulesets.Taiko
case ModType.Fun: case ModType.Fun:
return new Mod[] return new Mod[]
{ {
new MultiMod(new ModWindUp(), new ModWindDown()) new MultiMod(new ModWindUp(), new ModWindDown()),
new TaikoModMuted(),
}; };
default: default:

View File

@ -3,6 +3,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -24,12 +25,14 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
public class DrawableTaikoRuleset : DrawableScrollingRuleset<TaikoHitObject> public class DrawableTaikoRuleset : DrawableScrollingRuleset<TaikoHitObject>
{ {
private SkinnableDrawable scroller; public new BindableDouble TimeRange => base.TimeRange;
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping; protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
protected override bool UserScrollSpeedAdjustment => false; protected override bool UserScrollSpeedAdjustment => false;
private SkinnableDrawable scroller;
public DrawableTaikoRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) public DrawableTaikoRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.UI
/// <summary> /// <summary>
/// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="DrawableTaikoRuleset"/>. /// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="DrawableTaikoRuleset"/>.
/// </summary> /// </summary>
public const float DEFAULT_HEIGHT = 178; public const float DEFAULT_HEIGHT = 212;
private Container<HitExplosion> hitExplosionContainer; private Container<HitExplosion> hitExplosionContainer;
private Container<KiaiHitExplosion> kiaiExplosionContainer; private Container<KiaiHitExplosion> kiaiExplosionContainer;

View File

@ -0,0 +1,41 @@
// 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;
using NUnit.Framework;
using osu.Game.Extensions;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class TimeDisplayExtensionTest
{
private static readonly object[][] editor_formatted_duration_tests =
{
new object[] { new TimeSpan(0, 0, 0, 0, 50), "00:00:050" },
new object[] { new TimeSpan(0, 0, 0, 10, 50), "00:10:050" },
new object[] { new TimeSpan(0, 0, 5, 10), "05:10:000" },
new object[] { new TimeSpan(0, 1, 5, 10), "65:10:000" },
};
[TestCaseSource(nameof(editor_formatted_duration_tests))]
public void TestEditorFormat(TimeSpan input, string expectedOutput)
{
Assert.AreEqual(expectedOutput, input.ToEditorFormattedString());
}
private static readonly object[][] formatted_duration_tests =
{
new object[] { new TimeSpan(0, 0, 10), "00:10" },
new object[] { new TimeSpan(0, 5, 10), "05:10" },
new object[] { new TimeSpan(1, 5, 10), "01:05:10" },
new object[] { new TimeSpan(1, 1, 5, 10), "01:01:05:10" },
};
[TestCaseSource(nameof(formatted_duration_tests))]
public void TestFormattedDuration(TimeSpan input, string expectedOutput)
{
Assert.AreEqual(expectedOutput, input.ToFormattedDuration().ToString());
}
}
}

View File

@ -168,8 +168,8 @@ namespace osu.Game.Tests.Online
public override async Task<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) public override async Task<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
{ {
await AllowImport.Task; await AllowImport.Task.ConfigureAwait(false);
return await (CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)); return await (CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false);
} }
} }

View File

@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Hitcircle button not clickable", () => !hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Enabled.Value); AddAssert("Hitcircle button not clickable", () => !hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Enabled.Value);
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddAssert("Hitcircle button is clickable", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Enabled.Value); AddAssert("Hitcircle button is clickable", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Enabled.Value);
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Click()); AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
AddAssert("Tool changed", () => hitObjectComposer.ChildrenOfType<ComposeBlueprintContainer>().First().CurrentTool is HitCircleCompositionTool); AddAssert("Tool changed", () => hitObjectComposer.ChildrenOfType<ComposeBlueprintContainer>().First().CurrentTool is HitCircleCompositionTool);
} }

View File

@ -228,7 +228,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var lastAction = pauseOverlay.OnRetry; var lastAction = pauseOverlay.OnRetry;
pauseOverlay.OnRetry = () => triggered = true; pauseOverlay.OnRetry = () => triggered = true;
getButton(1).Click(); getButton(1).TriggerClick();
pauseOverlay.OnRetry = lastAction; pauseOverlay.OnRetry = lastAction;
}); });

View File

@ -13,6 +13,7 @@ using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Tests.Visual.Multiplayer;
@ -25,41 +26,43 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private readonly User streamingUser = new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Test user" }; private readonly User streamingUser = new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Test user" };
[Cached(typeof(SpectatorClient))]
private TestSpectatorClient testSpectatorClient = new TestSpectatorClient();
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache(); private UserLookupCache lookupCache = new TestUserLookupCache();
// used just to show beatmap card for the time being. // used just to show beatmap card for the time being.
protected override bool UseOnlineAPI => true; protected override bool UseOnlineAPI => true;
private SoloSpectator spectatorScreen;
[Resolved] [Resolved]
private OsuGameBase game { get; set; } private OsuGameBase game { get; set; }
private BeatmapSetInfo importedBeatmap; private TestSpectatorClient spectatorClient;
private SoloSpectator spectatorScreen;
private BeatmapSetInfo importedBeatmap;
private int importedBeatmapId; private int importedBeatmapId;
public override void SetUpSteps() [SetUpSteps]
public void SetupSteps()
{ {
base.SetUpSteps(); DependenciesScreen dependenciesScreen = null;
AddStep("load dependencies", () =>
{
spectatorClient = new TestSpectatorClient();
// The screen gets suspended so it stops receiving updates.
Child = spectatorClient;
LoadScreen(dependenciesScreen = new DependenciesScreen(spectatorClient));
});
AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded);
AddStep("import beatmap", () => AddStep("import beatmap", () =>
{ {
importedBeatmap = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result; importedBeatmap = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result;
importedBeatmapId = importedBeatmap.Beatmaps.First(b => b.RulesetID == 0).OnlineBeatmapID ?? -1; importedBeatmapId = importedBeatmap.Beatmaps.First(b => b.RulesetID == 0).OnlineBeatmapID ?? -1;
}); });
AddStep("add streaming client", () =>
{
Remove(testSpectatorClient);
Add(testSpectatorClient);
});
finish();
} }
[Test] [Test]
@ -206,22 +209,36 @@ namespace osu.Game.Tests.Visual.Gameplay
private void waitForPlayer() => AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true); 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", () => spectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId));
private void finish() => AddStep("end play", () => testSpectatorClient.EndPlay(streamingUser.Id)); private void finish() => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id));
private void checkPaused(bool state) => private void checkPaused(bool state) =>
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state); AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
private void sendFrames(int count = 10) private void sendFrames(int count = 10)
{ {
AddStep("send frames", () => testSpectatorClient.SendFrames(streamingUser.Id, count)); AddStep("send frames", () => spectatorClient.SendFrames(streamingUser.Id, count));
} }
private void loadSpectatingScreen() private void loadSpectatingScreen()
{ {
AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(streamingUser))); AddStep("load spectator", () => LoadScreen(spectatorScreen = new SoloSpectator(streamingUser)));
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded);
} }
/// <summary>
/// Used for the sole purpose of adding <see cref="TestSpectatorClient"/> as a resolvable dependency.
/// </summary>
private class DependenciesScreen : OsuScreen
{
[Cached(typeof(SpectatorClient))]
public readonly TestSpectatorClient Client;
public DependenciesScreen(TestSpectatorClient client)
{
Client = client;
}
}
} }
} }

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("has 2 rooms", () => container.Rooms.Count == 2); AddAssert("has 2 rooms", () => container.Rooms.Count == 2);
AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0)); AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0));
AddStep("select first room", () => container.Rooms.First().Click()); AddStep("select first room", () => container.Rooms.First().TriggerClick());
AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
} }

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -88,6 +89,28 @@ namespace osu.Game.Tests.Visual.Multiplayer
// used to test the flow of multiplayer from visual tests. // used to test the flow of multiplayer from visual tests.
} }
[Test]
public void TestCreateRoomViaKeyboard()
{
// create room dialog
AddStep("Press new document", () => InputManager.Keys(PlatformAction.DocumentNew));
AddUntilStep("wait for settings", () => InputManager.ChildrenOfType<MultiplayerMatchSettingsOverlay>().FirstOrDefault() != null);
// edit playlist item
AddStep("Press select", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for song select", () => InputManager.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault() != null);
// select beatmap
AddStep("Press select", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for return to screen", () => InputManager.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault() == null);
// create room
AddStep("Press select", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => client.Room != null);
}
[Test] [Test]
public void TestCreateRoomWithoutPassword() public void TestCreateRoomWithoutPassword()
{ {
@ -209,7 +232,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
DrawableRoom.PasswordEntryPopover passwordEntryPopover = null; DrawableRoom.PasswordEntryPopover passwordEntryPopover = null;
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableRoom.PasswordEntryPopover>().FirstOrDefault()) != null); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password"); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().Click()); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => client.Room != null); AddUntilStep("wait for join", () => client.Room != null);
@ -373,7 +396,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
}); });
AddStep("open mod overlay", () => this.ChildrenOfType<PurpleTriangleButton>().ElementAt(2).Click()); AddStep("open mod overlay", () => this.ChildrenOfType<PurpleTriangleButton>().ElementAt(2).TriggerClick());
AddStep("invoke on back button", () => multiplayerScreen.OnBackButton()); AddStep("invoke on back button", () => multiplayerScreen.OnBackButton());
@ -381,7 +404,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden); AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
testLeave("lounge tab item", () => this.ChildrenOfType<BreadcrumbControl<IScreen>.BreadcrumbTabItem>().First().Click()); testLeave("lounge tab item", () => this.ChildrenOfType<BreadcrumbControl<IScreen>.BreadcrumbTabItem>().First().TriggerClick());
testLeave("back button", () => multiplayerScreen.OnBackButton()); testLeave("back button", () => multiplayerScreen.OnBackButton());

View File

@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableRoom.PasswordEntryPopover>().FirstOrDefault()) != null); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password"); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().Click()); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First()); AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
AddAssert("room join password correct", () => lastJoinedPassword == "password"); AddAssert("room join password correct", () => lastJoinedPassword == "password");

View File

@ -0,0 +1,191 @@
// 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;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneTeamVersus : ScreenTestScene
{
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
private DependenciesScreen dependenciesScreen;
private TestMultiplayer multiplayerScreen;
private TestMultiplayerClient client;
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
}
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("import beatmap", () =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
});
AddStep("create multiplayer screen", () => multiplayerScreen = new TestMultiplayer());
AddStep("load dependencies", () =>
{
client = new TestMultiplayerClient(multiplayerScreen.RoomManager);
// The screen gets suspended so it stops receiving updates.
Child = client;
LoadScreen(dependenciesScreen = new DependenciesScreen(client));
});
AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded);
AddStep("load multiplayer", () => LoadScreen(multiplayerScreen));
AddUntilStep("wait for multiplayer to load", () => multiplayerScreen.IsLoaded);
AddUntilStep("wait for lounge to load", () => this.ChildrenOfType<MultiplayerLoungeSubScreen>().FirstOrDefault()?.IsLoaded == true);
}
[Test]
public void TestCreateWithType()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Type = { Value = MatchType.TeamVersus },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
AddAssert("user state arrived", () => client.Room?.Users.FirstOrDefault()?.MatchState is TeamVersusUserState);
}
[Test]
public void TestChangeTeamsViaButton()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Type = { Value = MatchType.TeamVersus },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddAssert("user on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
AddStep("press button", () =>
{
InputManager.MoveMouseTo(multiplayerScreen.ChildrenOfType<TeamDisplay>().First());
InputManager.Click(MouseButton.Left);
});
AddAssert("user on team 1", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1);
AddStep("press button", () => InputManager.Click(MouseButton.Left));
AddAssert("user on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
}
[Test]
public void TestChangeTypeViaMatchSettings()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddAssert("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead);
AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus));
AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
}
private void createRoom(Func<Room> room)
{
AddStep("open room", () =>
{
multiplayerScreen.OpenNewRoom(room());
});
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2);
AddStep("create room", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for join", () => client.Room != null);
}
/// <summary>
/// Used for the sole purpose of adding <see cref="TestMultiplayerClient"/> as a resolvable dependency.
/// </summary>
private class DependenciesScreen : OsuScreen
{
[Cached(typeof(MultiplayerClient))]
public readonly TestMultiplayerClient Client;
public DependenciesScreen(TestMultiplayerClient client)
{
Client = client;
}
}
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
{
public new TestRequestHandlingMultiplayerRoomManager RoomManager { get; private set; }
protected override RoomManager CreateRoomManager() => RoomManager = new TestRequestHandlingMultiplayerRoomManager();
}
}
}

View File

@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("show manually", () => accountCreation.Show()); AddStep("show manually", () => accountCreation.Show());
AddUntilStep("overlay is visible", () => accountCreation.State.Value == Visibility.Visible); AddUntilStep("overlay is visible", () => accountCreation.State.Value == Visibility.Visible);
AddStep("click button", () => accountCreation.ChildrenOfType<SettingsButton>().Single().Click()); AddStep("click button", () => accountCreation.ChildrenOfType<SettingsButton>().Single().TriggerClick());
AddUntilStep("warning screen is present", () => accountCreation.ChildrenOfType<ScreenWarning>().SingleOrDefault()?.IsPresent == true); AddUntilStep("warning screen is present", () => accountCreation.ChildrenOfType<ScreenWarning>().SingleOrDefault()?.IsPresent == true);
AddStep("log back in", () => API.Login("dummy", "password")); AddStep("log back in", () => API.Login("dummy", "password"));

View File

@ -330,22 +330,11 @@ namespace osu.Game.Tests.Visual.Online
InputManager.ReleaseKey(Key.AltLeft); InputManager.ReleaseKey(Key.AltLeft);
} }
private void pressCloseDocumentKeys() => pressKeysFor(PlatformAction.DocumentClose); private void pressCloseDocumentKeys() => InputManager.Keys(PlatformAction.DocumentClose);
private void pressNewTabKeys() => pressKeysFor(PlatformAction.TabNew); private void pressNewTabKeys() => InputManager.Keys(PlatformAction.TabNew);
private void pressRestoreTabKeys() => pressKeysFor(PlatformAction.TabRestore); private void pressRestoreTabKeys() => InputManager.Keys(PlatformAction.TabRestore);
private void pressKeysFor(PlatformAction type)
{
var binding = host.PlatformKeyBindings.First(b => (PlatformAction)b.Action == type);
foreach (var k in binding.KeyCombination.Keys)
InputManager.PressKey((Key)k);
foreach (var k in binding.KeyCombination.Keys)
InputManager.ReleaseKey((Key)k);
}
private void clickDrawable(Drawable d) private void clickDrawable(Drawable d)
{ {

View File

@ -1,13 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
@ -21,51 +20,44 @@ namespace osu.Game.Tests.Visual.Online
{ {
private readonly User streamingUser = new User { Id = 2, Username = "Test user" }; private readonly User streamingUser = new User { Id = 2, Username = "Test user" };
[Cached(typeof(SpectatorClient))] private TestSpectatorClient spectatorClient;
private TestSpectatorClient testSpectatorClient = new TestSpectatorClient();
private CurrentlyPlayingDisplay currentlyPlaying; private CurrentlyPlayingDisplay currentlyPlaying;
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
private Container nestedContainer;
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
AddStep("add streaming client", () => AddStep("add streaming client", () =>
{ {
nestedContainer?.Remove(testSpectatorClient); spectatorClient = new TestSpectatorClient();
Remove(lookupCache); var lookupCache = new TestUserLookupCache();
Children = new Drawable[] Children = new Drawable[]
{ {
lookupCache, lookupCache,
nestedContainer = new Container spectatorClient,
new DependencyProvidingContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] CachedDependencies = new (Type, object)[]
{ {
testSpectatorClient, (typeof(SpectatorClient), spectatorClient),
currentlyPlaying = new CurrentlyPlayingDisplay (typeof(UserLookupCache), lookupCache)
},
Child = currentlyPlaying = new CurrentlyPlayingDisplay
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
} }
}
}, },
}; };
}); });
AddStep("Reset players", () => testSpectatorClient.EndPlay(streamingUser.Id));
} }
[Test] [Test]
public void TestBasicDisplay() public void TestBasicDisplay()
{ {
AddStep("Add playing user", () => testSpectatorClient.StartPlay(streamingUser.Id, 0)); AddStep("Add playing user", () => spectatorClient.StartPlay(streamingUser.Id, 0));
AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType<UserGridPanel>()?.FirstOrDefault()?.User.Id == 2); AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType<UserGridPanel>()?.FirstOrDefault()?.User.Id == 2);
AddStep("Remove playing user", () => testSpectatorClient.EndPlay(streamingUser.Id)); AddStep("Remove playing user", () => spectatorClient.EndPlay(streamingUser.Id));
AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType<UserGridPanel>().Any()); AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType<UserGridPanel>().Any());
} }

View File

@ -0,0 +1,87 @@
// 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;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Comments;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneDrawableComment : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private Container container;
[SetUp]
public void SetUp() => Schedule(() =>
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
container = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
};
});
[TestCaseSource(nameof(comments))]
public void TestComment(string description, string text)
{
AddStep(description, () =>
{
comment.Message = text;
container.Add(new DrawableComment(comment));
});
}
private static readonly Comment comment = new Comment
{
Id = 1,
LegacyName = "Test User",
CreatedAt = DateTimeOffset.Now,
VotesCount = 0,
};
private static object[] comments =
{
new[] { "Plain", "This is plain comment" },
new[] { "Link", "Please visit https://osu.ppy.sh" },
new[]
{
"Heading", @"# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6"
},
// Taken from https://github.com/ppy/osu/issues/13993#issuecomment-885994077
new[]
{
"Problematic", @"My tablet doesn't work :(
It's a Huion 420 and it's apparently incompatible with OpenTablet Driver. The warning I get is: ""DeviceInUseException: Device is currently in use by another kernel module. To fix this issue, please follow the instructions from https://github.com/OpenTabletDriver/OpenTabletDriver/wiki/Linux-FAQ#arg umentoutofrangeexception-value-0-15"" and it repeats 4 times on the notification before logging subsequent warnings.
Checking the logs, it looks for other Huion tablets before sending the notification (e.g.
""2021-07-23 03:52:33 [verbose]: Detect: Searching for tablet 'Huion WH1409 V2'
20 2021-07-23 03:52:33 [error]: DeviceInUseException: Device is currently in use by another kernel module. To fix this issue, please follow the instructions from https://github.com/OpenTabletDriver/OpenTabletDriver/wiki/Linux-FAQ#arg umentoutofrangeexception-value-0-15"")
I use an Arch based installation of Linux and the tablet runs perfectly with Digimend kernel driver, with area configuration, pen pressure, etc. On osu!lazer the cursor disappears until I set it to ""Borderless"" instead of ""Fullscreen"" and even after it shows up, it goes to the bottom left corner as soon as a map starts.
I have honestly 0 idea of whats going on at this point."
}
};
}
}

View File

@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestControl() public void TestControl()
{ {
AddAssert("Front page selected", () => header.Current.Value == "frontpage"); AddAssert("Front page selected", () => header.Current.Value == NewsHeader.FrontPageString);
AddAssert("1 tab total", () => header.TabCount == 1); AddAssert("1 tab total", () => header.TabCount == 1);
AddStep("Set article 1", () => header.SetArticle("1")); AddStep("Set article 1", () => header.SetArticle("1"));
@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("2 tabs total", () => header.TabCount == 2); AddAssert("2 tabs total", () => header.TabCount == 2);
AddStep("Set front page", () => header.SetFrontPage()); AddStep("Set front page", () => header.SetFrontPage());
AddAssert("Front page selected", () => header.Current.Value == "frontpage"); AddAssert("Front page selected", () => header.Current.Value == NewsHeader.FrontPageString);
AddAssert("1 tab total", () => header.TabCount == 1); AddAssert("1 tab total", () => header.TabCount == 1);
} }

View File

@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Show", () => overlay.Show()); AddStep("Show", () => overlay.Show());
AddUntilStep("Show More button is visible", () => showMoreButton?.Alpha == 1); AddUntilStep("Show More button is visible", () => showMoreButton?.Alpha == 1);
setUpNewsResponse(responseWithNoCursor, "Set up no cursor response"); setUpNewsResponse(responseWithNoCursor, "Set up no cursor response");
AddStep("Click Show More", () => showMoreButton?.Click()); AddStep("Click Show More", () => showMoreButton?.TriggerClick());
AddUntilStep("Show More button is hidden", () => showMoreButton?.Alpha == 0); AddUntilStep("Show More button is hidden", () => showMoreButton?.Alpha == 0);
} }

View File

@ -32,19 +32,19 @@ namespace osu.Game.Tests.Visual.Online
} }
}); });
AddStep("click button", () => button.Click()); AddStep("click button", () => button.TriggerClick());
AddAssert("action fired once", () => fireCount == 1); AddAssert("action fired once", () => fireCount == 1);
AddAssert("is in loading state", () => button.IsLoading); AddAssert("is in loading state", () => button.IsLoading);
AddStep("click button", () => button.Click()); AddStep("click button", () => button.TriggerClick());
AddAssert("action not fired", () => fireCount == 1); AddAssert("action not fired", () => fireCount == 1);
AddAssert("is in loading state", () => button.IsLoading); AddAssert("is in loading state", () => button.IsLoading);
AddUntilStep("wait for loaded", () => !button.IsLoading); AddUntilStep("wait for loaded", () => !button.IsLoading);
AddStep("click button", () => button.Click()); AddStep("click button", () => button.TriggerClick());
AddAssert("action fired twice", () => fireCount == 2); AddAssert("action fired twice", () => fireCount == 2);
AddAssert("is in loading state", () => button.IsLoading); AddAssert("is in loading state", () => button.IsLoading);

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Log in", logIn); AddStep("Log in", logIn);
AddStep("User comment", () => addVotePill(getUserComment())); AddStep("User comment", () => addVotePill(getUserComment()));
AddAssert("Background is transparent", () => votePill.Background.Alpha == 0); AddAssert("Background is transparent", () => votePill.Background.Alpha == 0);
AddStep("Click", () => votePill.Click()); AddStep("Click", () => votePill.TriggerClick());
AddAssert("Not loading", () => !votePill.IsLoading); AddAssert("Not loading", () => !votePill.IsLoading);
} }
@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Log in", logIn); AddStep("Log in", logIn);
AddStep("Random comment", () => addVotePill(getRandomComment())); AddStep("Random comment", () => addVotePill(getRandomComment()));
AddAssert("Background is visible", () => votePill.Background.Alpha == 1); AddAssert("Background is visible", () => votePill.Background.Alpha == 1);
AddStep("Click", () => votePill.Click()); AddStep("Click", () => votePill.TriggerClick());
AddAssert("Loading", () => votePill.IsLoading); AddAssert("Loading", () => votePill.IsLoading);
} }
@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Hide login overlay", () => login.Hide()); AddStep("Hide login overlay", () => login.Hide());
AddStep("Log out", API.Logout); AddStep("Log out", API.Logout);
AddStep("Random comment", () => addVotePill(getRandomComment())); AddStep("Random comment", () => addVotePill(getRandomComment()));
AddStep("Click", () => votePill.Click()); AddStep("Click", () => votePill.TriggerClick());
AddAssert("Login overlay is visible", () => login.State.Value == Visibility.Visible); AddAssert("Login overlay is visible", () => login.State.Value == Visibility.Visible);
} }

View File

@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestWikiHeader() public void TestWikiHeader()
{ {
AddAssert("Current is index", () => checkCurrent("index")); AddAssert("Current is index", () => checkCurrent(WikiHeader.IndexPageString));
AddStep("Change wiki page data", () => wikiPageData.Value = new APIWikiPage AddStep("Change wiki page data", () => wikiPageData.Value = new APIWikiPage
{ {
@ -54,8 +54,8 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Current is welcome", () => checkCurrent("Welcome")); AddAssert("Current is welcome", () => checkCurrent("Welcome"));
AddAssert("Check breadcrumb", checkBreadcrumb); AddAssert("Check breadcrumb", checkBreadcrumb);
AddStep("Change current to index", () => header.Current.Value = "index"); AddStep("Change current to index", () => header.Current.Value = WikiHeader.IndexPageString);
AddAssert("Current is index", () => checkCurrent("index")); AddAssert("Current is index", () => checkCurrent(WikiHeader.IndexPageString));
AddStep("Change wiki page data", () => wikiPageData.Value = new APIWikiPage AddStep("Change wiki page data", () => wikiPageData.Value = new APIWikiPage
{ {
@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Check breadcrumb", checkBreadcrumb); AddAssert("Check breadcrumb", checkBreadcrumb);
} }
private bool checkCurrent(string expectedCurrent) => header.Current.Value == expectedCurrent; private bool checkCurrent(LocalisableString expectedCurrent) => header.Current.Value == expectedCurrent;
private bool checkBreadcrumb() private bool checkBreadcrumb()
{ {

View File

@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0]));
AddStep("select last room", () => roomsContainer.Rooms[^1].Click()); AddStep("select last room", () => roomsContainer.Rooms[^1].TriggerClick());
AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.Rooms[0])); AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.Rooms[0]));
AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms[^1])); AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms[^1]));

View File

@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Playlists
}); });
}); });
AddStep("start match", () => match.ChildrenOfType<PlaylistsReadyButton>().First().Click()); AddStep("start match", () => match.ChildrenOfType<PlaylistsReadyButton>().First().TriggerClick());
AddUntilStep("player loader loaded", () => Stack.CurrentScreen is PlayerLoader); AddUntilStep("player loader loaded", () => Stack.CurrentScreen is PlayerLoader);
} }

View File

@ -160,7 +160,7 @@ namespace osu.Game.Tests.Visual.Settings
{ {
var resetButton = settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First(); var resetButton = settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First();
resetButton.Click(); resetButton.TriggerClick();
}); });
AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha == 0); AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha == 0);
@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.Settings
{ {
var resetButton = panel.ChildrenOfType<ResetButton>().First(); var resetButton = panel.ChildrenOfType<ResetButton>().First();
resetButton.Click(); resetButton.TriggerClick();
}); });
AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha == 0); AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha == 0);

View File

@ -143,9 +143,9 @@ namespace osu.Game.Tests.Visual.SongSelect
public override async Task<StarDifficulty> GetDifficultyAsync(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo = null, IEnumerable<Mod> mods = null, CancellationToken cancellationToken = default) public override async Task<StarDifficulty> GetDifficultyAsync(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo = null, IEnumerable<Mod> mods = null, CancellationToken cancellationToken = default)
{ {
if (blockCalculation) if (blockCalculation)
await calculationBlocker.Task; await calculationBlocker.Task.ConfigureAwait(false);
return await base.GetDifficultyAsync(beatmapInfo, rulesetInfo, mods, cancellationToken); return await base.GetDifficultyAsync(beatmapInfo, rulesetInfo, mods, cancellationToken).ConfigureAwait(false);
} }
} }
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -32,6 +33,7 @@ using osuTK.Graphics;
namespace osu.Game.Tests.Visual namespace osu.Game.Tests.Visual
{ {
[TestFixture] [TestFixture]
[HeadlessTest]
public class TestSceneOsuGame : OsuTestScene public class TestSceneOsuGame : OsuTestScene
{ {
private IReadOnlyList<Type> requiredGameDependencies => new[] private IReadOnlyList<Type> requiredGameDependencies => new[]
@ -83,10 +85,15 @@ namespace osu.Game.Tests.Visual
typeof(PreviewTrackManager), typeof(PreviewTrackManager),
}; };
private OsuGame game;
[Resolved]
private OsuGameBase gameBase { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, OsuGameBase gameBase) private void load(GameHost host)
{ {
OsuGame game = new OsuGame(); game = new OsuGame();
game.SetHost(host); game.SetHost(host);
Children = new Drawable[] Children = new Drawable[]
@ -100,7 +107,39 @@ namespace osu.Game.Tests.Visual
}; };
AddUntilStep("wait for load", () => game.IsLoaded); AddUntilStep("wait for load", () => game.IsLoaded);
}
[Test]
public void TestNullRulesetHandled()
{
RulesetInfo ruleset = null;
AddStep("store current ruleset", () => ruleset = Ruleset.Value);
AddStep("set global ruleset to null value", () => Ruleset.Value = null);
AddAssert("ruleset still valid", () => Ruleset.Value.Available);
AddAssert("ruleset unchanged", () => ReferenceEquals(Ruleset.Value, ruleset));
}
[Test]
public void TestUnavailableRulesetHandled()
{
RulesetInfo ruleset = null;
AddStep("store current ruleset", () => ruleset = Ruleset.Value);
AddStep("set global ruleset to invalid value", () => Ruleset.Value = new RulesetInfo
{
Name = "unavailable",
Available = false,
});
AddAssert("ruleset still valid", () => Ruleset.Value.Available);
AddAssert("ruleset unchanged", () => ReferenceEquals(Ruleset.Value, ruleset));
}
[Test]
public void TestAvailableDependencies()
{
AddAssert("check OsuGame DI members", () => AddAssert("check OsuGame DI members", () =>
{ {
foreach (var type in requiredGameDependencies) foreach (var type in requiredGameDependencies)
@ -111,6 +150,7 @@ namespace osu.Game.Tests.Visual
return true; return true;
}); });
AddAssert("check OsuGameBase DI members", () => AddAssert("check OsuGameBase DI members", () =>
{ {
foreach (var type in requiredGameBaseDependencies) foreach (var type in requiredGameBaseDependencies)

View File

@ -1,16 +1,21 @@
// 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 NUnit.Framework; using NUnit.Framework;
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.Utils; using osu.Framework.Utils;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
public class TestSceneLabelledColourPalette : OsuTestScene public class TestSceneLabelledColourPalette : OsuManualInputManagerTestScene
{ {
private LabelledColourPalette component; private LabelledColourPalette component;
@ -30,10 +35,29 @@ namespace osu.Game.Tests.Visual.UserInterface
}, 8); }, 8);
} }
[Test]
public void TestUserInteractions()
{
createColourPalette();
assertColourCount(4);
clickAddColour();
assertColourCount(5);
deleteFirstColour();
assertColourCount(4);
clickFirstColour();
AddAssert("colour picker spawned", () => this.ChildrenOfType<OsuColourPicker>().Any());
}
private void createColourPalette(bool hasDescription = false) private void createColourPalette(bool hasDescription = false)
{ {
AddStep("create component", () => AddStep("create component", () =>
{ {
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new Container Child = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -46,6 +70,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre, Origin = Anchor.Centre,
ColourNamePrefix = "My colour #" ColourNamePrefix = "My colour #"
} }
}
}; };
component.Label = "a sample component"; component.Label = "a sample component";
@ -53,18 +78,49 @@ namespace osu.Game.Tests.Visual.UserInterface
component.Colours.AddRange(new[] component.Colours.AddRange(new[]
{ {
Color4.DarkRed, Colour4.DarkRed,
Color4.Aquamarine, Colour4.Aquamarine,
Color4.Goldenrod, Colour4.Goldenrod,
Color4.Gainsboro Colour4.Gainsboro
}); });
}); });
} }
private Color4 randomColour() => new Color4( private Colour4 randomColour() => new Color4(
RNG.NextSingle(), RNG.NextSingle(),
RNG.NextSingle(), RNG.NextSingle(),
RNG.NextSingle(), RNG.NextSingle(),
1); 1);
private void assertColourCount(int count) => AddAssert($"colour count is {count}", () => component.Colours.Count == count);
private void clickAddColour() => AddStep("click new colour button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ColourPalette.AddColourButton>().Single());
InputManager.Click(MouseButton.Left);
});
private void clickFirstColour() => AddStep("click first colour", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ColourDisplay>().First());
InputManager.Click(MouseButton.Left);
});
private void deleteFirstColour()
{
AddStep("right-click first colour", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ColourDisplay>().First());
InputManager.Click(MouseButton.Right);
});
AddUntilStep("wait for menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
AddStep("click delete", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<DrawableOsuMenuItem>().Single());
InputManager.Click(MouseButton.Left);
});
}
} }
} }

View File

@ -94,10 +94,10 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2); AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2);
AddStep("deselect", () => modSelect.DeselectAllButton.Click()); AddStep("deselect", () => modSelect.DeselectAllButton.TriggerClick());
AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0); AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0);
AddStep("reselect", () => modSelect.GetModButton(osuModDoubleTime).Click()); AddStep("reselect", () => modSelect.GetModButton(osuModDoubleTime).TriggerClick());
AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true); AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true);
} }

View File

@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("wait for button load", () => modSelect.ButtonsLoaded); AddUntilStep("wait for button load", () => modSelect.ButtonsLoaded);
AddStep("select mod", () => modSelect.SelectMod(testCustomisableMod)); AddStep("select mod", () => modSelect.SelectMod(testCustomisableMod));
AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value); AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value);
AddStep("open Customisation", () => modSelect.CustomiseButton.Click()); AddStep("open Customisation", () => modSelect.CustomiseButton.TriggerClick());
AddStep("deselect mod", () => modSelect.SelectMod(testCustomisableMod)); AddStep("deselect mod", () => modSelect.SelectMod(testCustomisableMod));
AddAssert("controls hidden", () => modSelect.ModSettingsContainer.State.Value == Visibility.Hidden); AddAssert("controls hidden", () => modSelect.ModSettingsContainer.State.Value == Visibility.Hidden);
} }

View File

@ -1,21 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Tournament.Configuration;
using osu.Game.Tests; using osu.Game.Tests;
using osu.Game.Tournament.Configuration;
namespace osu.Game.Tournament.Tests.NonVisual namespace osu.Game.Tournament.Tests.NonVisual
{ {
[TestFixture] [TestFixture]
public class CustomTourneyDirectoryTest public class CustomTourneyDirectoryTest : TournamentHostTest
{ {
[Test] [Test]
public void TestDefaultDirectory() public void TestDefaultDirectory()
@ -24,7 +21,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
{ {
try try
{ {
var osu = loadOsu(host); var osu = LoadTournament(host);
var storage = osu.Dependencies.Get<Storage>(); var storage = osu.Dependencies.Get<Storage>();
Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default"))); Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default")));
@ -54,7 +51,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
try try
{ {
var osu = loadOsu(host); var osu = LoadTournament(host);
storage = osu.Dependencies.Get<Storage>(); storage = osu.Dependencies.Get<Storage>();
@ -111,7 +108,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
try try
{ {
var osu = loadOsu(host); var osu = LoadTournament(host);
var storage = osu.Dependencies.Get<Storage>(); var storage = osu.Dependencies.Get<Storage>();
@ -151,25 +148,6 @@ namespace osu.Game.Tournament.Tests.NonVisual
} }
} }
private TournamentGameBase loadOsu(GameHost host)
{
var osu = new TournamentGameBase();
Task.Run(() => host.Run(osu))
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return osu;
}
private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 90000)
{
Task task = Task.Run(() =>
{
while (!result()) Thread.Sleep(200);
});
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
private string basePath(string testInstance) => Path.Combine(RuntimeInfo.StartupDirectory, "headless", testInstance); private string basePath(string testInstance) => Path.Combine(RuntimeInfo.StartupDirectory, "headless", testInstance);
} }
} }

View File

@ -0,0 +1,45 @@
// 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.IO;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform;
using osu.Game.Rulesets;
using osu.Game.Tests;
namespace osu.Game.Tournament.Tests.NonVisual
{
public class DataLoadTest : TournamentHostTest
{
[Test]
public void TestUnavailableRuleset()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUnavailableRuleset)))
{
try
{
var osu = new TestTournament();
LoadTournament(host, osu);
var storage = osu.Dependencies.Get<Storage>();
Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default")));
}
finally
{
host.Exit();
}
}
}
public class TestTournament : TournamentGameBase
{
[BackgroundDependencyLoader]
private void load()
{
Ruleset.Value = new RulesetInfo(); // not available
}
}
}
}

View File

@ -1,10 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -15,7 +12,7 @@ using osu.Game.Tournament.IPC;
namespace osu.Game.Tournament.Tests.NonVisual namespace osu.Game.Tournament.Tests.NonVisual
{ {
[TestFixture] [TestFixture]
public class IPCLocationTest public class IPCLocationTest : TournamentHostTest
{ {
[Test] [Test]
public void CheckIPCLocation() public void CheckIPCLocation()
@ -34,11 +31,11 @@ namespace osu.Game.Tournament.Tests.NonVisual
try try
{ {
var osu = loadOsu(host); var osu = LoadTournament(host);
TournamentStorage storage = (TournamentStorage)osu.Dependencies.Get<Storage>(); TournamentStorage storage = (TournamentStorage)osu.Dependencies.Get<Storage>();
FileBasedIPC ipc = null; FileBasedIPC ipc = null;
waitForOrAssert(() => (ipc = osu.Dependencies.Get<MatchIPCInfo>() as FileBasedIPC) != null, @"ipc could not be populated in a reasonable amount of time"); WaitForOrAssert(() => (ipc = osu.Dependencies.Get<MatchIPCInfo>() as FileBasedIPC) != null, @"ipc could not be populated in a reasonable amount of time");
Assert.True(ipc.SetIPCLocation(testStableInstallDirectory)); Assert.True(ipc.SetIPCLocation(testStableInstallDirectory));
Assert.True(storage.AllTournaments.Exists("stable.json")); Assert.True(storage.AllTournaments.Exists("stable.json"));
@ -51,24 +48,5 @@ namespace osu.Game.Tournament.Tests.NonVisual
} }
} }
} }
private TournamentGameBase loadOsu(GameHost host)
{
var osu = new TournamentGameBase();
Task.Run(() => host.Run(osu))
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return osu;
}
private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 90000)
{
Task task = Task.Run(() =>
{
while (!result()) Thread.Sleep(200);
});
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
} }
} }

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 System;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Platform;
namespace osu.Game.Tournament.Tests.NonVisual
{
public abstract class TournamentHostTest
{
public static TournamentGameBase LoadTournament(GameHost host, TournamentGameBase tournament = null)
{
tournament ??= new TournamentGameBase();
Task.Run(() => host.Run(tournament))
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
WaitForOrAssert(() => tournament.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return tournament;
}
public static void WaitForOrAssert(Func<bool> result, string failureMessage, int timeout = 90000)
{
Task task = Task.Run(() =>
{
while (!result()) Thread.Sleep(200);
});
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
}
}

View File

@ -40,6 +40,6 @@ namespace osu.Game.Tournament.Tests.Screens
() => this.ChildrenOfType<TeamScore>().All(score => score.Alpha == (visible ? 1 : 0))); () => this.ChildrenOfType<TeamScore>().All(score => score.Alpha == (visible ? 1 : 0)));
private void toggleWarmup() private void toggleWarmup()
=> AddStep("toggle warmup", () => this.ChildrenOfType<TourneyButton>().First().Click()); => AddStep("toggle warmup", () => this.ChildrenOfType<TourneyButton>().First().TriggerClick());
} }
} }

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 System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -11,6 +10,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Extensions;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
@ -198,8 +198,8 @@ namespace osu.Game.Tournament.Components
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
new DiffPiece(("Length", TimeSpan.FromMilliseconds(length).ToString(@"mm\:ss"))), new DiffPiece(("Length", length.ToFormattedDuration().ToString())),
new DiffPiece(("BPM", $"{bpm:0.#}")) new DiffPiece(("BPM", $"{bpm:0.#}")),
} }
}, },
new Container new Container

View File

@ -27,6 +27,9 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
{ {
var teams = new List<TournamentTeam>(); var teams = new List<TournamentTeam>();
if (!storage.Exists(teams_filename))
return teams;
try try
{ {
using (Stream stream = storage.GetStream(teams_filename, FileAccess.Read, FileMode.Open)) using (Stream stream = storage.GetStream(teams_filename, FileAccess.Read, FileMode.Open))

View File

@ -9,11 +9,13 @@ using System.Threading.Tasks;
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.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osu.Game.Tournament.Screens.Drawings.Components; using osu.Game.Tournament.Screens.Drawings.Components;
@ -51,6 +53,29 @@ namespace osu.Game.Tournament.Screens.Drawings
if (!TeamList.Teams.Any()) if (!TeamList.Teams.Any())
{ {
LinkFlowContainer links;
InternalChildren = new Drawable[]
{
new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = 0.3f,
},
new WarningBox("No drawings.txt file found. Please create one and restart the client."),
links = new LinkFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = 60,
AutoSizeAxes = Axes.Both
}
};
links.AddLink("Click for details on the file format", "https://osu.ppy.sh/wiki/en/Tournament_Drawings", t => t.Colour = Color4.White);
return; return;
} }

View File

@ -66,7 +66,9 @@ namespace osu.Game.Tournament
} }
ladder ??= new LadderInfo(); ladder ??= new LadderInfo();
ladder.Ruleset.Value ??= RulesetStore.AvailableRulesets.First();
ladder.Ruleset.Value = RulesetStore.GetRuleset(ladder.Ruleset.Value?.ShortName)
?? RulesetStore.AvailableRulesets.First();
bool addedInfo = false; bool addedInfo = false;

View File

@ -1,2 +1,3 @@
[*.cs] [*.cs]
dotnet_diagnostic.OLOC001.prefix_namespace = osu.Game.Resources.Localisation dotnet_diagnostic.OLOC001.prefix_namespace = osu.Game.Resources.Localisation
dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text.

View File

@ -1,22 +1,34 @@
// 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.Localisation; using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
[LocalisableEnum(typeof(BeatmapSetOnlineStatusEnumLocalisationMapper))]
public enum BeatmapSetOnlineStatus public enum BeatmapSetOnlineStatus
{ {
None = -3, None = -3,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusGraveyard))]
Graveyard = -2, Graveyard = -2,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusWip))]
WIP = -1, WIP = -1,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusPending))]
Pending = 0, Pending = 0,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusRanked))]
Ranked = 1, Ranked = 1,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusApproved))]
Approved = 2, Approved = 2,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusQualified))]
Qualified = 3, Qualified = 3,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusLoved))]
Loved = 4, Loved = 4,
} }
@ -25,40 +37,4 @@ namespace osu.Game.Beatmaps
public static bool GrantsPerformancePoints(this BeatmapSetOnlineStatus status) public static bool GrantsPerformancePoints(this BeatmapSetOnlineStatus status)
=> status == BeatmapSetOnlineStatus.Ranked || status == BeatmapSetOnlineStatus.Approved; => status == BeatmapSetOnlineStatus.Ranked || status == BeatmapSetOnlineStatus.Approved;
} }
public class BeatmapSetOnlineStatusEnumLocalisationMapper : EnumLocalisationMapper<BeatmapSetOnlineStatus>
{
public override LocalisableString Map(BeatmapSetOnlineStatus value)
{
switch (value)
{
case BeatmapSetOnlineStatus.None:
return string.Empty;
case BeatmapSetOnlineStatus.Graveyard:
return BeatmapsetsStrings.ShowStatusGraveyard;
case BeatmapSetOnlineStatus.WIP:
return BeatmapsetsStrings.ShowStatusWip;
case BeatmapSetOnlineStatus.Pending:
return BeatmapsetsStrings.ShowStatusPending;
case BeatmapSetOnlineStatus.Ranked:
return BeatmapsetsStrings.ShowStatusRanked;
case BeatmapSetOnlineStatus.Approved:
return BeatmapsetsStrings.ShowStatusApproved;
case BeatmapSetOnlineStatus.Qualified:
return BeatmapsetsStrings.ShowStatusQualified;
case BeatmapSetOnlineStatus.Loved:
return BeatmapsetsStrings.ShowStatusLoved;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
} }

View File

@ -1,26 +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 System;
namespace osu.Game.Extensions
{
public static class EditorDisplayExtensions
{
/// <summary>
/// Get an editor formatted string (mm:ss:mss)
/// </summary>
/// <param name="milliseconds">A time value in milliseconds.</param>
/// <returns>An editor formatted display string.</returns>
public static string ToEditorFormattedString(this double milliseconds) =>
ToEditorFormattedString(TimeSpan.FromMilliseconds(milliseconds));
/// <summary>
/// Get an editor formatted string (mm:ss:mss)
/// </summary>
/// <param name="timeSpan">A time value.</param>
/// <returns>An editor formatted display string.</returns>
public static string ToEditorFormattedString(this TimeSpan timeSpan) =>
$"{(timeSpan < TimeSpan.Zero ? "-" : string.Empty)}{timeSpan:mm\\:ss\\:fff}";
}
}

View File

@ -0,0 +1,51 @@
// 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;
using osu.Framework.Localisation;
namespace osu.Game.Extensions
{
public static class TimeDisplayExtensions
{
/// <summary>
/// Get an editor formatted string (mm:ss:mss)
/// </summary>
/// <param name="milliseconds">A time value in milliseconds.</param>
/// <returns>An editor formatted display string.</returns>
public static string ToEditorFormattedString(this double milliseconds) =>
ToEditorFormattedString(TimeSpan.FromMilliseconds(milliseconds));
/// <summary>
/// Get an editor formatted string (mm:ss:mss)
/// </summary>
/// <param name="timeSpan">A time value.</param>
/// <returns>An editor formatted display string.</returns>
public static string ToEditorFormattedString(this TimeSpan timeSpan) =>
$"{(timeSpan < TimeSpan.Zero ? "-" : string.Empty)}{(int)timeSpan.TotalMinutes:00}:{timeSpan:ss\\:fff}";
/// <summary>
/// Get a formatted duration (dd:hh:mm:ss with days/hours omitted if zero).
/// </summary>
/// <param name="milliseconds">A duration in milliseconds.</param>
/// <returns>A formatted duration string.</returns>
public static LocalisableString ToFormattedDuration(this double milliseconds) =>
ToFormattedDuration(TimeSpan.FromMilliseconds(milliseconds));
/// <summary>
/// Get a formatted duration (dd:hh:mm:ss with days/hours omitted if zero).
/// </summary>
/// <param name="timeSpan">A duration value.</param>
/// <returns>A formatted duration string.</returns>
public static LocalisableString ToFormattedDuration(this TimeSpan timeSpan)
{
if (timeSpan.TotalDays >= 1)
return new LocalisableFormattableString(timeSpan, @"dd\:hh\:mm\:ss");
if (timeSpan.TotalHours >= 1)
return new LocalisableFormattableString(timeSpan, @"hh\:mm\:ss");
return new LocalisableFormattableString(timeSpan, @"mm\:ss");
}
}
}

View File

@ -10,6 +10,7 @@ using System.Collections.Generic;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Users; using osu.Game.Users;
@ -25,6 +26,9 @@ namespace osu.Game.Graphics.Containers
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private OsuGame game { get; set; } private OsuGame game { get; set; }
[Resolved]
private GameHost host { get; set; }
public void AddLinks(string text, List<Link> links) public void AddLinks(string text, List<Link> links)
{ {
if (string.IsNullOrEmpty(text) || links == null) if (string.IsNullOrEmpty(text) || links == null)
@ -91,8 +95,11 @@ namespace osu.Game.Graphics.Containers
{ {
if (action != null) if (action != null)
action(); action();
else else if (game != null)
game?.HandleLink(link); game.HandleLink(link);
// fallback to handle cases where OsuGame is not available, ie. tournament client.
else if (link.Action == LinkAction.External)
host.OpenUrlExternally(link.Argument);
}, },
}); });
} }

View File

@ -48,7 +48,7 @@ namespace osu.Game.Graphics.Containers.Markdown
public override SpriteText CreateSpriteText() => new OsuSpriteText public override SpriteText CreateSpriteText() => new OsuSpriteText
{ {
Font = OsuFont.GetFont(size: 14), Font = OsuFont.GetFont(Typeface.Inter, size: 14, weight: FontWeight.Regular),
}; };
public override MarkdownTextFlowContainer CreateTextFlow() => new OsuMarkdownTextFlowContainer(); public override MarkdownTextFlowContainer CreateTextFlow() => new OsuMarkdownTextFlowContainer();

View File

@ -70,7 +70,7 @@ namespace osu.Game.Graphics.Containers.Markdown
public FontWeight FontWeight; public FontWeight FontWeight;
protected override SpriteText CreateSpriteText() protected override SpriteText CreateSpriteText()
=> base.CreateSpriteText().With(t => t.Font = t.Font.With(size: FontSize, weight: FontWeight)); => base.CreateSpriteText().With(t => t.Font = t.Font.With(Typeface.Torus, size: FontSize, weight: FontWeight));
} }
} }
} }

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -59,7 +60,9 @@ namespace osu.Game.Graphics.Containers
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
InternalChild = new GridContainer InternalChildren = new Drawable[]
{
new GridContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
@ -86,6 +89,8 @@ namespace osu.Game.Graphics.Containers
}, },
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
},
new HoverClickSounds()
}; };
} }

View File

@ -21,6 +21,8 @@ namespace osu.Game.Graphics
public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular); public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular);
public static FontUsage Inter => GetFont(Typeface.Inter, weight: FontWeight.Regular);
/// <summary> /// <summary>
/// Retrieves a <see cref="FontUsage"/>. /// Retrieves a <see cref="FontUsage"/>.
/// </summary> /// </summary>
@ -54,6 +56,9 @@ namespace osu.Game.Graphics
case Typeface.Torus: case Typeface.Torus:
return "Torus"; return "Torus";
case Typeface.Inter:
return "Inter";
} }
return null; return null;
@ -107,7 +112,8 @@ namespace osu.Game.Graphics
public enum Typeface public enum Typeface
{ {
Venera, Venera,
Torus Torus,
Inter,
} }
public enum FontWeight public enum FontWeight

View File

@ -20,7 +20,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
Size = TwoLayerButton.SIZE_EXTENDED; Size = TwoLayerButton.SIZE_EXTENDED;
Child = button = new TwoLayerButton Child = button = new TwoLayerButton(HoverSampleSet.Submit)
{ {
Anchor = Anchor.TopLeft, Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft, Origin = Anchor.TopLeft,
@ -35,7 +35,7 @@ namespace osu.Game.Graphics.UserInterface
Add(receptor = new Receptor()); Add(receptor = new Receptor());
} }
receptor.OnBackPressed = () => button.Click(); receptor.OnBackPressed = () => button.TriggerClick();
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -56,6 +56,7 @@ namespace osu.Game.Graphics.UserInterface
private Vector2 hoverSpacing => new Vector2(3f, 0f); private Vector2 hoverSpacing => new Vector2(3f, 0f);
public DialogButton() public DialogButton()
: base(HoverSampleSet.Submit)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;

View File

@ -23,14 +23,20 @@ namespace osu.Game.Graphics.UserInterface
[Resolved] [Resolved]
private GameHost host { get; set; } private GameHost host { get; set; }
private readonly SpriteIcon linkIcon;
public ExternalLinkButton(string link = null) public ExternalLinkButton(string link = null)
{ {
Link = link; Link = link;
Size = new Vector2(12); Size = new Vector2(12);
InternalChild = new SpriteIcon InternalChildren = new Drawable[]
{
linkIcon = new SpriteIcon
{ {
Icon = FontAwesome.Solid.ExternalLinkAlt, Icon = FontAwesome.Solid.ExternalLinkAlt,
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
},
new HoverClickSounds(HoverSampleSet.Submit)
}; };
} }
@ -42,13 +48,13 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
InternalChild.FadeColour(hoverColour, 500, Easing.OutQuint); linkIcon.FadeColour(hoverColour, 500, Easing.OutQuint);
return base.OnHover(e); return base.OnHover(e);
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
InternalChild.FadeColour(Color4.White, 500, Easing.OutQuint); linkIcon.FadeColour(Color4.White, 500, Easing.OutQuint);
base.OnHoverLost(e); base.OnHoverLost(e);
} }

View File

@ -10,8 +10,8 @@ namespace osu.Game.Graphics.UserInterface
[Description("default")] [Description("default")]
Default, Default,
[Description("soft")] [Description("submit")]
Soft, Submit,
[Description("button")] [Description("button")]
Button, Button,

View File

@ -71,7 +71,8 @@ namespace osu.Game.Graphics.UserInterface
} }
} }
public TwoLayerButton() public TwoLayerButton(HoverSampleSet sampleSet = HoverSampleSet.Default)
: base(sampleSet)
{ {
Size = SIZE_RETRACTED; Size = SIZE_RETRACTED;
Shear = shear; Shear = shear;

View File

@ -1,32 +1,39 @@
// 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.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
/// <summary> /// <summary>
/// A component which displays a colour along with related description text. /// A component which displays a colour along with related description text.
/// </summary> /// </summary>
public class ColourDisplay : CompositeDrawable, IHasCurrentValue<Color4> public class ColourDisplay : CompositeDrawable, IHasCurrentValue<Colour4>
{ {
private readonly BindableWithCurrent<Color4> current = new BindableWithCurrent<Color4>(); /// <summary>
/// Invoked when the user has requested the colour corresponding to this <see cref="ColourDisplay"/>
/// to be removed from its palette.
/// </summary>
public event Action<ColourDisplay> DeleteRequested;
private readonly BindableWithCurrent<Colour4> current = new BindableWithCurrent<Colour4>();
private Box fill;
private OsuSpriteText colourHexCode;
private OsuSpriteText colourName; private OsuSpriteText colourName;
public Bindable<Color4> Current public Bindable<Colour4> Current
{ {
get => current.Current; get => current.Current;
set => current.Current = value; set => current.Current = value;
@ -62,11 +69,37 @@ namespace osu.Game.Graphics.UserInterfaceV2
Spacing = new Vector2(0, 10), Spacing = new Vector2(0, 10),
Children = new Drawable[] Children = new Drawable[]
{ {
new CircularContainer new ColourCircle
{ {
RelativeSizeAxes = Axes.X, Current = { BindTarget = Current },
Height = 100, DeleteRequested = () => DeleteRequested?.Invoke(this)
Masking = true, },
colourName = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
}
}
};
}
private class ColourCircle : OsuClickableContainer, IHasPopover, IHasContextMenu
{
public Bindable<Colour4> Current { get; } = new Bindable<Colour4>();
public Action DeleteRequested { get; set; }
private readonly Box fill;
private readonly OsuSpriteText colourHexCode;
public ColourCircle()
{
RelativeSizeAxes = Axes.X;
Height = 100;
CornerRadius = 50;
Masking = true;
Action = this.ShowPopover;
Children = new Drawable[] Children = new Drawable[]
{ {
fill = new Box fill = new Box
@ -79,14 +112,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
Origin = Anchor.Centre, Origin = Anchor.Centre,
Font = OsuFont.Default.With(size: 12) Font = OsuFont.Default.With(size: 12)
} }
}
},
colourName = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
}
}
}; };
} }
@ -94,14 +119,28 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
base.LoadComplete(); base.LoadComplete();
current.BindValueChanged(_ => updateColour(), true); Current.BindValueChanged(_ => updateColour(), true);
} }
private void updateColour() private void updateColour()
{ {
fill.Colour = current.Value; fill.Colour = Current.Value;
colourHexCode.Text = current.Value.ToHex(); colourHexCode.Text = Current.Value.ToHex();
colourHexCode.Colour = OsuColour.ForegroundTextColourFor(current.Value); colourHexCode.Colour = OsuColour.ForegroundTextColourFor(Current.Value);
}
public Popover GetPopover() => new OsuPopover(false)
{
Child = new OsuColourPicker
{
Current = { BindTarget = Current }
}
};
public MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem("Delete", MenuItemType.Destructive, () => DeleteRequested?.Invoke())
};
} }
} }
} }

View File

@ -1,14 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
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.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
@ -17,7 +22,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
/// </summary> /// </summary>
public class ColourPalette : CompositeDrawable public class ColourPalette : CompositeDrawable
{ {
public BindableList<Color4> Colours { get; } = new BindableList<Color4>(); public BindableList<Colour4> Colours { get; } = new BindableList<Colour4>();
private string colourNamePrefix = "Colour"; private string colourNamePrefix = "Colour";
@ -36,36 +41,24 @@ namespace osu.Game.Graphics.UserInterfaceV2
} }
} }
private FillFlowContainer<ColourDisplay> palette; private FillFlowContainer palette;
private Container placeholder;
private IEnumerable<ColourDisplay> colourDisplays => palette.OfType<ColourDisplay>();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
AutoSizeDuration = fade_duration;
AutoSizeEasing = Easing.OutQuint;
InternalChildren = new Drawable[] InternalChild = palette = new FillFlowContainer
{
palette = new FillFlowContainer<ColourDisplay>
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10), Spacing = new Vector2(10),
Direction = FillDirection.Full Direction = FillDirection.Full
},
placeholder = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new OsuSpriteText
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Text = "(none)",
Font = OsuFont.Default.With(weight: FontWeight.Bold)
}
}
}; };
} }
@ -73,7 +66,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
base.LoadComplete(); base.LoadComplete();
Colours.BindCollectionChanged((_, __) => updatePalette(), true); Colours.BindCollectionChanged((_, args) =>
{
if (args.Action != NotifyCollectionChangedAction.Replace)
updatePalette();
}, true);
FinishTransforms(true); FinishTransforms(true);
} }
@ -83,37 +80,103 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
palette.Clear(); palette.Clear();
if (Colours.Any()) for (int i = 0; i < Colours.Count; ++i)
{ {
palette.FadeIn(fade_duration, Easing.OutQuint); // copy to avoid accesses to modified closure.
placeholder.FadeOut(fade_duration, Easing.OutQuint); int colourIndex = i;
} ColourDisplay display;
else
palette.Add(display = new ColourDisplay
{ {
palette.FadeOut(fade_duration, Easing.OutQuint); Current = { Value = Colours[colourIndex] }
placeholder.FadeIn(fade_duration, Easing.OutQuint); });
display.Current.BindValueChanged(colour => Colours[colourIndex] = colour.NewValue);
display.DeleteRequested += colourDeletionRequested;
} }
foreach (var item in Colours) palette.Add(new AddColourButton
{ {
palette.Add(new ColourDisplay Action = () => Colours.Add(Colour4.White)
{
Current = { Value = item }
}); });
}
reindexItems(); reindexItems();
} }
private void colourDeletionRequested(ColourDisplay display) => Colours.RemoveAt(palette.IndexOf(display));
private void reindexItems() private void reindexItems()
{ {
int index = 1; int index = 1;
foreach (var colour in palette) foreach (var colourDisplay in colourDisplays)
{ {
colour.ColourName = $"{colourNamePrefix} {index}"; colourDisplay.ColourName = $"{colourNamePrefix} {index}";
index += 1; index += 1;
} }
} }
internal class AddColourButton : CompositeDrawable
{
public Action Action
{
set => circularButton.Action = value;
}
private readonly OsuClickableContainer circularButton;
public AddColourButton()
{
AutoSizeAxes = Axes.Y;
Width = 100;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
circularButton = new OsuClickableContainer
{
RelativeSizeAxes = Axes.X,
Height = 100,
CornerRadius = 50,
Masking = true,
BorderThickness = 5,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.Transparent,
AlwaysPresent = true
},
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(20),
Icon = FontAwesome.Solid.Plus
}
}
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = "New"
}
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
circularButton.BorderColour = colours.BlueDarker;
}
}
} }
} }

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 osuTK.Graphics; using osu.Framework.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
@ -13,7 +13,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
} }
public BindableList<Color4> Colours => Component.Colours; public BindableList<Colour4> Colours => Component.Colours;
public string ColourNamePrefix public string ColourNamePrefix
{ {

View File

@ -32,7 +32,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
Depth = 1 Depth = 1
}, },
new HoverClickSounds(HoverSampleSet.Soft) new HoverClickSounds()
}); });
} }

View File

@ -57,7 +57,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
Depth = 1 Depth = 1
}, },
new HoverClickSounds(HoverSampleSet.Soft) new HoverClickSounds()
}); });
} }

View File

@ -0,0 +1,44 @@
// 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.Localisation;
namespace osu.Game.Localisation
{
public static class NamedOverlayComponentStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.NamedOverlayComponent";
/// <summary>
/// "browse for new beatmaps"
/// </summary>
public static LocalisableString BeatmapListingDescription => new TranslatableString(getKey(@"beatmap_listing_description"), @"browse for new beatmaps");
/// <summary>
/// "track recent dev updates in the osu! ecosystem"
/// </summary>
public static LocalisableString ChangelogDescription => new TranslatableString(getKey(@"changelog_description"), @"track recent dev updates in the osu! ecosystem");
/// <summary>
/// "view your friends and other information"
/// </summary>
public static LocalisableString DashboardDescription => new TranslatableString(getKey(@"dashboard_description"), @"view your friends and other information");
/// <summary>
/// "find out who's the best right now"
/// </summary>
public static LocalisableString RankingsDescription => new TranslatableString(getKey(@"rankings_description"), @"find out who's the best right now");
/// <summary>
/// "get up-to-date on community happenings"
/// </summary>
public static LocalisableString NewsDescription => new TranslatableString(getKey(@"news_description"), @"get up-to-date on community happenings");
/// <summary>
/// "knowledge base"
/// </summary>
public static LocalisableString WikiDescription => new TranslatableString(getKey(@"wiki_description"), @"knowledge base");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -150,7 +150,7 @@ namespace osu.Game.Online.API
userReq.Failure += ex => userReq.Failure += ex =>
{ {
if (ex.InnerException is WebException webException && webException.Message == @"Unauthorized") if (ex is WebException webException && webException.Message == @"Unauthorized")
{ {
log.Add(@"Login no longer valid"); log.Add(@"Login no longer valid");
Logout(); Logout();
@ -257,8 +257,8 @@ namespace osu.Game.Online.API
this.password = password; this.password = password;
} }
public IHubClientConnector GetHubConnector(string clientName, string endpoint) => public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) =>
new HubClientConnector(clientName, endpoint, this, versionHash); new HubClientConnector(clientName, endpoint, this, versionHash, preferMessagePack);
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
{ {

View File

@ -86,8 +86,6 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
private APIRequestCompletionState completionState; private APIRequestCompletionState completionState;
private Action pendingFailure;
public void Perform(IAPIProvider api) public void Perform(IAPIProvider api)
{ {
if (!(api is APIAccess apiAccess)) if (!(api is APIAccess apiAccess))
@ -99,29 +97,23 @@ namespace osu.Game.Online.API
API = apiAccess; API = apiAccess;
User = apiAccess.LocalUser.Value; User = apiAccess.LocalUser.Value;
if (checkAndScheduleFailure()) if (isFailing) return;
return;
WebRequest = CreateWebRequest(); WebRequest = CreateWebRequest();
WebRequest.Failed += Fail; WebRequest.Failed += Fail;
WebRequest.AllowRetryOnTimeout = false; WebRequest.AllowRetryOnTimeout = false;
WebRequest.AddHeader("Authorization", $"Bearer {API.AccessToken}"); WebRequest.AddHeader("Authorization", $"Bearer {API.AccessToken}");
if (checkAndScheduleFailure()) if (isFailing) return;
return;
if (!WebRequest.Aborted) // could have been aborted by a Cancel() call
{
Logger.Log($@"Performing request {this}", LoggingTarget.Network); Logger.Log($@"Performing request {this}", LoggingTarget.Network);
WebRequest.Perform(); WebRequest.Perform();
}
if (checkAndScheduleFailure()) if (isFailing) return;
return;
PostProcess(); PostProcess();
API.Schedule(TriggerSuccess); TriggerSuccess();
} }
/// <summary> /// <summary>
@ -141,7 +133,10 @@ namespace osu.Game.Online.API
completionState = APIRequestCompletionState.Completed; completionState = APIRequestCompletionState.Completed;
} }
if (API == null)
Success?.Invoke(); Success?.Invoke();
else
API.Schedule(() => Success?.Invoke());
} }
internal void TriggerFailure(Exception e) internal void TriggerFailure(Exception e)
@ -154,7 +149,10 @@ namespace osu.Game.Online.API
completionState = APIRequestCompletionState.Failed; completionState = APIRequestCompletionState.Failed;
} }
if (API == null)
Failure?.Invoke(e); Failure?.Invoke(e);
else
API.Schedule(() => Failure?.Invoke(e));
} }
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled")); public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
@ -163,17 +161,18 @@ namespace osu.Game.Online.API
{ {
lock (completionStateLock) lock (completionStateLock)
{ {
// while it doesn't matter if code following this check is run more than once,
// this avoids unnecessarily performing work where we are already sure the user has been informed.
if (completionState != APIRequestCompletionState.Waiting) if (completionState != APIRequestCompletionState.Waiting)
return; return;
}
WebRequest?.Abort(); WebRequest?.Abort();
// in the case of a cancellation we don't care about whether there's an error in the response.
if (!(e is OperationCanceledException))
{
string responseString = WebRequest?.GetResponseString(); string responseString = WebRequest?.GetResponseString();
if (!string.IsNullOrEmpty(responseString)) // naive check whether there's an error in the response to avoid unnecessary JSON deserialisation.
if (!string.IsNullOrEmpty(responseString) && responseString.Contains(@"""error"""))
{ {
try try
{ {
@ -186,31 +185,23 @@ namespace osu.Game.Online.API
{ {
} }
} }
}
Logger.Log($@"Failing request {this} ({e})", LoggingTarget.Network); Logger.Log($@"Failing request {this} ({e})", LoggingTarget.Network);
pendingFailure = () => TriggerFailure(e); TriggerFailure(e);
checkAndScheduleFailure(); }
} }
/// <summary> /// <summary>
/// Checked for cancellation or error. Also queues up the Failed event if we can. /// Whether this request is in a failing or failed state.
/// </summary> /// </summary>
/// <returns>Whether we are in a failed or cancelled state.</returns> private bool isFailing
private bool checkAndScheduleFailure() {
get
{ {
lock (completionStateLock) lock (completionStateLock)
{
if (pendingFailure == null)
return completionState == APIRequestCompletionState.Failed; return completionState == APIRequestCompletionState.Failed;
} }
if (API == null)
pendingFailure();
else
API.Schedule(pendingFailure);
pendingFailure = null;
return true;
} }
private class DisplayableError private class DisplayableError

View File

@ -89,7 +89,7 @@ namespace osu.Game.Online.API
state.Value = APIState.Offline; state.Value = APIState.Offline;
} }
public IHubClientConnector GetHubConnector(string clientName, string endpoint) => null; public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
{ {

View File

@ -102,7 +102,8 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
/// <param name="clientName">The name of the client this connector connects for, used for logging.</param> /// <param name="clientName">The name of the client this connector connects for, used for logging.</param>
/// <param name="endpoint">The endpoint to the hub.</param> /// <param name="endpoint">The endpoint to the hub.</param>
IHubClientConnector? GetHubConnector(string clientName, string endpoint); /// <param name="preferMessagePack">Whether to use MessagePack for serialisation if available on this platform.</param>
IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true);
/// <summary> /// <summary>
/// Create a new user account. This is a blocking operation. /// Create a new user account. This is a blocking operation.

View File

@ -31,6 +31,7 @@ namespace osu.Game.Online.Chat
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new LinkHoverSounds(sampleSet, Parts); protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new LinkHoverSounds(sampleSet, Parts);
public DrawableLinkCompiler(IEnumerable<Drawable> parts) public DrawableLinkCompiler(IEnumerable<Drawable> parts)
: base(HoverSampleSet.Submit)
{ {
Parts = parts.ToList(); Parts = parts.ToList();
} }

View File

@ -26,6 +26,7 @@ namespace osu.Game.Online
private readonly string clientName; private readonly string clientName;
private readonly string endpoint; private readonly string endpoint;
private readonly string versionHash; private readonly string versionHash;
private readonly bool preferMessagePack;
private readonly IAPIProvider api; private readonly IAPIProvider api;
/// <summary> /// <summary>
@ -51,12 +52,14 @@ namespace osu.Game.Online
/// <param name="endpoint">The endpoint to the hub.</param> /// <param name="endpoint">The endpoint to the hub.</param>
/// <param name="api"> An API provider used to react to connection state changes.</param> /// <param name="api"> An API provider used to react to connection state changes.</param>
/// <param name="versionHash">The hash representing the current game version, used for verification purposes.</param> /// <param name="versionHash">The hash representing the current game version, used for verification purposes.</param>
public HubClientConnector(string clientName, string endpoint, IAPIProvider api, string versionHash) /// <param name="preferMessagePack">Whether to use MessagePack for serialisation if available on this platform.</param>
public HubClientConnector(string clientName, string endpoint, IAPIProvider api, string versionHash, bool preferMessagePack = true)
{ {
this.clientName = clientName; this.clientName = clientName;
this.endpoint = endpoint; this.endpoint = endpoint;
this.api = api; this.api = api;
this.versionHash = versionHash; this.versionHash = versionHash;
this.preferMessagePack = preferMessagePack;
apiState.BindTo(api.State); apiState.BindTo(api.State);
apiState.BindValueChanged(state => apiState.BindValueChanged(state =>
@ -116,10 +119,7 @@ namespace osu.Game.Online
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Log($"{clientName} connection error: {e}", LoggingTarget.Network); await handleErrorAndDelay(e, cancellationToken).ConfigureAwait(false);
// retry on any failure.
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
} }
} }
} }
@ -129,6 +129,15 @@ namespace osu.Game.Online
} }
} }
/// <summary>
/// Handles an exception and delays an async flow.
/// </summary>
private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
{
Logger.Log($"{clientName} connection error: {exception}", LoggingTarget.Network);
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
}
private HubConnection buildConnection(CancellationToken cancellationToken) private HubConnection buildConnection(CancellationToken cancellationToken)
{ {
var builder = new HubConnectionBuilder() var builder = new HubConnectionBuilder()
@ -138,13 +147,19 @@ namespace osu.Game.Online
options.Headers.Add("OsuVersionHash", versionHash); options.Headers.Add("OsuVersionHash", versionHash);
}); });
if (RuntimeInfo.SupportsJIT) if (RuntimeInfo.SupportsJIT && preferMessagePack)
builder.AddMessagePackProtocol(); builder.AddMessagePackProtocol();
else else
{ {
// eventually we will precompile resolvers for messagepack, but this isn't working currently // eventually we will precompile resolvers for messagepack, but this isn't working currently
// see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308. // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308.
builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); builder.AddNewtonsoftJsonProtocol(options =>
{
options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
// TODO: This should only be required to be `TypeNameHandling.Auto`.
// See usage in osu-server-spectator for further documentation as to why this is required.
options.PayloadSerializerSettings.TypeNameHandling = TypeNameHandling.All;
});
} }
var newConnection = builder.Build(); var newConnection = builder.Build();
@ -155,17 +170,18 @@ namespace osu.Game.Online
return newConnection; return newConnection;
} }
private Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken) private async Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken)
{ {
isConnected.Value = false; isConnected.Value = false;
Logger.Log(ex != null ? $"{clientName} lost connection: {ex}" : $"{clientName} disconnected", LoggingTarget.Network); if (ex != null)
await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false);
else
Logger.Log($"{clientName} disconnected", LoggingTarget.Network);
// make sure a disconnect wasn't triggered (and this is still the active connection). // make sure a disconnect wasn't triggered (and this is still the active connection).
if (!cancellationToken.IsCancellationRequested) if (!cancellationToken.IsCancellationRequested)
Task.Run(connect, default); await Task.Run(connect, default).ConfigureAwait(false);
return Task.CompletedTask;
} }
private async Task disconnect(bool takeLock) private async Task disconnect(bool takeLock)

View File

@ -50,6 +50,25 @@ namespace osu.Game.Online.Multiplayer
/// <param name="state">The new state of the user.</param> /// <param name="state">The new state of the user.</param>
Task UserStateChanged(int userId, MultiplayerUserState state); Task UserStateChanged(int userId, MultiplayerUserState state);
/// <summary>
/// Signals that the match type state has changed for a user in this room.
/// </summary>
/// <param name="userId">The ID of the user performing a state change.</param>
/// <param name="state">The new state of the user.</param>
Task MatchUserStateChanged(int userId, MatchUserState state);
/// <summary>
/// Signals that the match type state has changed for this room.
/// </summary>
/// <param name="state">The new state of the room.</param>
Task MatchRoomStateChanged(MatchRoomState state);
/// <summary>
/// Send a match type specific request.
/// </summary>
/// <param name="e">The event to handle.</param>
Task MatchEvent(MatchServerEvent e);
/// <summary> /// <summary>
/// Signals that a user in this room changed their beatmap availability state. /// Signals that a user in this room changed their beatmap availability state.
/// </summary> /// </summary>

View File

@ -55,6 +55,12 @@ namespace osu.Game.Online.Multiplayer
/// <param name="newMods">The proposed new mods, excluding any required by the room itself.</param> /// <param name="newMods">The proposed new mods, excluding any required by the room itself.</param>
Task ChangeUserMods(IEnumerable<APIMod> newMods); Task ChangeUserMods(IEnumerable<APIMod> newMods);
/// <summary>
/// Send a match type specific request.
/// </summary>
/// <param name="request">The request to send.</param>
Task SendMatchRequest(MatchUserRequest request);
/// <summary> /// <summary>
/// As the host of a room, start the match. /// As the host of a room, start the match.
/// </summary> /// </summary>

View File

@ -0,0 +1,23 @@
// 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;
using MessagePack;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
#nullable enable
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// Room-wide state for the current match type.
/// Can be used to contain any state which should be used before or during match gameplay.
/// </summary>
[Serializable]
[MessagePackObject]
[Union(0, typeof(TeamVersusRoomState))]
// TODO: this will need to be abstract or interface when/if we get messagepack working. for now it isn't as it breaks json serialisation.
public class MatchRoomState
{
}
}

View File

@ -0,0 +1,17 @@
// 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;
using MessagePack;
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// An event from the server to allow clients to update gameplay to an expected state.
/// </summary>
[Serializable]
[MessagePackObject]
public abstract class MatchServerEvent
{
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using MessagePack;
#nullable enable
namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
{
public class ChangeTeamRequest : MatchUserRequest
{
[Key(0)]
public int TeamID { get; set; }
}
}

View File

@ -0,0 +1,21 @@
// 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;
using MessagePack;
#nullable enable
namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
{
[Serializable]
[MessagePackObject]
public class MultiplayerTeam
{
[Key(0)]
public int ID { get; set; }
[Key(1)]
public string Name { get; set; } = string.Empty;
}
}

Some files were not shown because too many files have changed in this diff Show More